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() != null) + { + GameObjectUtility.SetParentAndAlign(go, parent); + return; + } + + var canvas = Object.FindObjectOfType(); + if (canvas == null) + { + var canvasGo = new GameObject("Spline Demo Canvas", + typeof(Canvas), typeof(UnityEngine.UI.CanvasScaler), typeof(UnityEngine.UI.GraphicRaycaster)); + canvasGo.GetComponent().renderMode = RenderMode.ScreenSpaceOverlay; + Undo.RegisterCreatedObjectUndo(canvasGo, "Create Canvas For Spline Demo"); + } + + GameObjectUtility.SetParentAndAlign(go, canvas.gameObject); + } + + /// + /// 创建带 RectTransform 的 UI 节点;界面样条的计算范围就是这个矩形 + /// + private static GameObject CreateUiGameObject(string name, Vector2 size) + { + var go = new GameObject(name, typeof(RectTransform)); + var rt = go.GetComponent(); + rt.sizeDelta = size; + rt.anchorMin = new Vector2(0.5f, 0.5f); + rt.anchorMax = new Vector2(0.5f, 0.5f); + rt.pivot = new Vector2(0.5f, 0.5f); + return go; + } + + private static void Finish(GameObject go, string undoName) + { + Undo.RegisterCreatedObjectUndo(go, undoName); + Selection.activeObject = go; + } + } +} diff --git a/Editor/Spline/SplinePathCreateMenuItems.cs.meta b/Editor/Spline/SplinePathCreateMenuItems.cs.meta new file mode 100644 index 0000000..0e0d2d2 --- /dev/null +++ b/Editor/Spline/SplinePathCreateMenuItems.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 543e1ea7a6089fd6a835ee1d69cd721c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/XericLibraryEditor.dll b/Editor/XericLibraryEditor.dll index a3562df..e62e8df 100644 Binary files a/Editor/XericLibraryEditor.dll and b/Editor/XericLibraryEditor.dll differ diff --git a/Resources/Material/Graph/Lit/SceneGroundGraphs.shadergraph.meta b/Resources/Material/Graph/Lit/SceneGroundGraphs.shadergraph.meta index b507f4b..d196a94 100644 --- a/Resources/Material/Graph/Lit/SceneGroundGraphs.shadergraph.meta +++ b/Resources/Material/Graph/Lit/SceneGroundGraphs.shadergraph.meta @@ -7,4 +7,4 @@ ScriptedImporter: userData: assetBundleName: assetBundleVariant: - script: {instanceID: 0} + script: {fileID: 11500000, guid: 625f186215c104763be7675aa2d941aa, type: 3} diff --git a/Resources/Material/Graph/UI/Blueprint/BlueprintBackgound_GridLine.mat b/Resources/Material/Graph/UI/Blueprint/BlueprintBackgound_GridLine.mat index e16994a..7d7d792 100644 --- a/Resources/Material/Graph/UI/Blueprint/BlueprintBackgound_GridLine.mat +++ b/Resources/Material/Graph/UI/Blueprint/BlueprintBackgound_GridLine.mat @@ -48,6 +48,6 @@ Material: - _Color: {r: 1, g: 1, b: 1, a: 1} - _GridBackgroundColor: {r: 0.8666667, g: 0.8862745, b: 0.93333334, a: 1} - _GridColor: {r: 0.8666667, g: 0.8862745, b: 0.93333334, a: 1} - - _Transform: {r: 9.787723, g: 8.369877, b: -4.381715, a: -3.846801} + - _Transform: {r: 14.84472, g: 12.69432, b: 32.392414, a: -6.6925216} - _Vector0: {r: 0.95, g: 1, b: 0, a: 0} m_BuildTextureStacks: [] diff --git a/Resources/Material/Graph/UI/General/UISelectionBoxGraphs.shadergraph.meta b/Resources/Material/Graph/UI/General/UISelectionBoxGraphs.shadergraph.meta index c18ff9a..f1382c3 100644 --- a/Resources/Material/Graph/UI/General/UISelectionBoxGraphs.shadergraph.meta +++ b/Resources/Material/Graph/UI/General/UISelectionBoxGraphs.shadergraph.meta @@ -7,4 +7,4 @@ ScriptedImporter: userData: assetBundleName: assetBundleVariant: - script: {instanceID: 0} + script: {fileID: 11500000, guid: 625f186215c104763be7675aa2d941aa, type: 3} diff --git a/Resources/Material/Graph/UI/UI Graph/XericUICurveProgress.shader b/Resources/Material/Graph/UI/UI Graph/XericUICurveProgress.shader new file mode 100644 index 0000000..1d6af17 --- /dev/null +++ b/Resources/Material/Graph/UI/UI Graph/XericUICurveProgress.shader @@ -0,0 +1,159 @@ +Shader "XericLibrary/UIGraph/XericUICurveProgress" +{ + Properties + { + [PerRendererData] _MainTex ("Progress Texture", 2D) = "white" {} + _Color ("Tint", Color) = (1,1,1,1) + + _Tile ("Tile Count Along Path", Float) = 1 + _Scroll ("Scroll Speed (along +progress)", Float) = 0 + _ScrollPhase ("Scroll Phase", Range(0, 1)) = 0 + + _Cutoff ("Progress Cutoff", Range(0, 1)) = 1 + _CutoffSoftness ("Cutoff Softness", Range(0, 0.5)) = 0.02 + + _EdgeExp ("Edge Exp", Range(0.001, 1)) = 0.8671591 + _EdgeStep ("Edge Step", Range(0, 1)) = 0 + _EdgeStepRange ("Edge Step Range", Range(0, 0.5)) = 0.1 + + _StencilComp ("Stencil Comparison", Float) = 8 + _Stencil ("Stencil ID", Float) = 0 + _StencilOp ("Stencil Operation", Float) = 0 + _StencilWriteMask ("Stencil Write Mask", Float) = 255 + _StencilReadMask ("Stencil Read Mask", Float) = 255 + _ColorMask ("Color Mask", Float) = 15 + [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0 + } + + SubShader + { + Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" "CanUseSpriteAtlas"="True" } + + Stencil + { + Ref [_Stencil] + ReadMask [_StencilReadMask] + WriteMask [_StencilWriteMask] + Comp [_StencilComp] + Pass [_StencilOp] + } + + Cull Off + Lighting Off + ZWrite Off + ZTest [unity_GUIZTestMode] + Blend One OneMinusSrcAlpha + ColorMask [_ColorMask] + + Pass + { + Name "Default" + CGPROGRAM + #pragma vertex vert + #pragma fragment frag + #pragma target 3.0 + + #include "UnityCG.cginc" + #include "UnityUI.cginc" + + #pragma multi_compile_local _ UNITY_UI_CLIP_RECT + #pragma multi_compile_local _ UNITY_UI_ALPHACLIP + + struct appdata_t + { + float4 vertex : POSITION; + float4 color : COLOR; + float2 uv0 : TEXCOORD0; + float4 uv1 : TEXCOORD1; + UNITY_VERTEX_INPUT_INSTANCE_ID + }; + + struct v2f + { + float4 vertex : SV_POSITION; + fixed4 color : COLOR; + float2 uv0 : TEXCOORD0; + float4 worldPosition : TEXCOORD1; + float4 mask : TEXCOORD2; + float featherOn : TEXCOORD3; + UNITY_VERTEX_OUTPUT_STEREO + }; + + sampler2D _MainTex; + fixed4 _Color; + float4 _MainTex_ST; + float4 _ClipRect; + float _UIMaskSoftnessX; + float _UIMaskSoftnessY; + + float _Tile; + float _Scroll; + float _ScrollPhase; + float _Cutoff; + float _CutoffSoftness; + float _EdgeExp; + float _EdgeStep; + float _EdgeStepRange; + + v2f vert(appdata_t v) + { + v2f OUT; + UNITY_SETUP_INSTANCE_ID(v); + UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT); + + float4 vPosition = UnityObjectToClipPos(v.vertex); + OUT.worldPosition = v.vertex; + OUT.vertex = vPosition; + + float2 pixelSize = vPosition.w; + pixelSize /= float2(1, 1) * abs(mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy)); + + float4 clampedRect = clamp(_ClipRect, -2e10, 2e10); + OUT.mask = float4(v.vertex.xy * 2 - clampedRect.xy - clampedRect.zw, + 0.25 / (0.25 * half2(_UIMaskSoftnessX, _UIMaskSoftnessY) + abs(pixelSize.xy))); + + OUT.uv0 = v.uv0; + OUT.featherOn = v.uv1.x; + OUT.color = v.color * _Color; + return OUT; + } + + fixed4 frag(v2f IN) : SV_Target + { + // 与 UI 默认着色器同样的处理:把插值出来的 alpha 对齐到 1/256, + // 否则预乘混合下容易抖 + const half alphaPrecision = half(0xff); + const half invAlphaPrecision = half(1.0 / alphaPrecision); + IN.color.a = round(IN.color.a * alphaPrecision) * invAlphaPrecision; + + // uv0.y 已经是"沿整条面片的进度",横向不重复,纵向按 _Tile 平铺并可随时间流动 + float2 sampleUV = float2( + IN.uv0.x, + IN.uv0.y * _Tile - _Time.y * _Scroll + _ScrollPhase); + fixed4 tex = tex2D(_MainTex, sampleUV); + + float edge = smoothstep( + _EdgeStep - _EdgeStepRange * 2.0, + _EdgeStep + _EdgeStepRange, + sin(IN.uv0.x * UNITY_PI)); + float4 color = lerp(IN.color, IN.color * pow(saturate(edge), _EdgeExp), IN.featherOn); + color *= tex; + float soft = max(_CutoffSoftness, 1e-4); + color.a *= smoothstep(_Cutoff + soft, _Cutoff - soft, IN.uv0.y); + + #ifdef UNITY_UI_CLIP_RECT + color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect); + #endif + + #ifdef UNITY_UI_ALPHACLIP + clip(color.a - 0.001); + #endif + + color.rgb *= color.a; + return color; + } + ENDCG + } + } + Fallback Off +} diff --git a/Resources/Material/Graph/UI/UI Graph/XericUICurveProgress.shader.meta b/Resources/Material/Graph/UI/UI Graph/XericUICurveProgress.shader.meta new file mode 100644 index 0000000..61fec40 --- /dev/null +++ b/Resources/Material/Graph/UI/UI Graph/XericUICurveProgress.shader.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 6e11e3a532c926db17c9a5d958de6591 +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Blueprint/GraphTheoryBlueprintComponent.cs b/Runtime/Blueprint/GraphTheoryBlueprintComponent.cs index faa31f4..2787d40 100644 --- a/Runtime/Blueprint/GraphTheoryBlueprintComponent.cs +++ b/Runtime/Blueprint/GraphTheoryBlueprintComponent.cs @@ -5,6 +5,7 @@ using UnityEngine.InputSystem; #endif using XericLibrary.Runtime.Blueprint.Canvas; using XericLibrary.Runtime.Blueprint.Element; +using XericLibrary.Runtime.SuperStyleSheet; namespace XericLibrary.Runtime.Blueprint { @@ -33,7 +34,7 @@ namespace XericLibrary.Runtime.Blueprint [Header("Input")] [Tooltip("新输入系统 InputActionAsset。留空则仅支持鼠标基础操作(中键拖动平移、滚轮缩放)。")] [SerializeField] private Object _inputActions; - + public Object InputActions { get { return _inputActions; } diff --git a/Runtime/EPPlus.dll b/Runtime/EPPlus.dll index 31bc7ac..af76e9a 100644 Binary files a/Runtime/EPPlus.dll and b/Runtime/EPPlus.dll differ diff --git a/Runtime/Generation/Curve/SplineRibbonBuilder.cs b/Runtime/Generation/Curve/SplineRibbonBuilder.cs new file mode 100644 index 0000000..b89ed0f --- /dev/null +++ b/Runtime/Generation/Curve/SplineRibbonBuilder.cs @@ -0,0 +1,504 @@ +using System; +using System.Collections.Generic; + +using UnityEngine; + +namespace XericLibrary.Runtime.UIGraph +{ + /// + /// 一条面片切出来的一个贝塞尔小块:几何、逐点宽度、端点颜色、uv 进度区间。 + /// 位置都在 Graphic 的局部平面内,与渲染器同一坐标约定。 + /// + internal struct RibbonPiece + { + public Vector2 c0; + public Vector2 c1; + public Vector2 c2; + public Vector2 c3; + public float w0; + public float w1; + public float w2; + public float w3; + public Color32 startColor; + public Color32 endColor; + public float uvYStart; + public float uvYEnd; + public int ribbonIndex; + /// 本小块占它所在样条段的比例,用来折算细分数 + public float segmentFraction; + } + + /// 面片上 uv0.y 代表什么进度 + public enum SplineUvProgressSpace + { + /// 每条面片各自 0→1:尾部 0、头部 1,效果跟着已填充部分伸缩 + RibbonLocal, + /// 钉在整条样条上:贴图位置不随进度变化 + SplineGlobal, + } + + /// + /// 样条面片切分器:把若干条 (头部,尾部) 面片切成逐段贝塞尔小块。 + /// + /// 纯函数:不碰 MonoBehaviour,也不碰 Debug.Log —— 后者在 Unity 之外会抛 + /// "ECall could not be defined",那样就没法在离线门里直接跑出数来验证。 + /// 几何切分用贝塞尔在 [t0,t1] 上的精确限制(Restrict); + /// t0=0,t1=1 时逐点还原原始四个控制点,所以整段不产生任何漂移。 + /// + /// + internal static class SplineRibbonBuilder + { + internal struct Request + { + /// 各控制点锚点(局部坐标),长度 = pointCount + public Vector2[] anchors; + /// 各点的出手柄(局部坐标世界式,即已含锚点) + public Vector2[] outHandles; + /// 各点的入手柄(局部坐标世界式,即已含锚点) + public Vector2[] inHandles; + /// 各点颜色 + public Color[] pointColors; + /// 各点宽度 + public float[] pointWidths; + + public int pointCount; + public int segmentCount; + public bool curveLoop; + + /// 偶数下标=头部、奇数下标=尾部,成对;取值 [0,1] + public float[] headTailPairs; + /// 可空;与面片同序时逐条叠乘到样条颜色上 + public Color32[] ribbonTints; + + /// 进度 → 参数 t(距离校正就落在这个回调里) + public Func progressToParametric; + + public SplineUvProgressSpace uvSpace; + /// 每条面片至少切成几块;决定 uv0.y 与视觉距离对齐的精度 + public int chunksPerRibbon; + } + + /// + /// 切分所有面片。无效输入返回空表而不是抛异常。 + /// + internal static List Build(Request req) + { + var pieces = new List(); + + if (req.anchors == null || req.outHandles == null || req.inHandles == null) + return pieces; + if (req.headTailPairs == null || req.progressToParametric == null) + return pieces; + if (req.pointCount < 2 || req.segmentCount < 1 || req.headTailPairs.Length < 2) + return pieces; + + int chunks = Mathf.Max(1, req.chunksPerRibbon); + int ribbonCount = req.headTailPairs.Length / 2; + + for (int r = 0; r < ribbonCount; r++) + { + float head = Mathf.Clamp01(req.headTailPairs[r * 2]); + float tail = Mathf.Clamp01(req.headTailPairs[r * 2 + 1]); + if (Mathf.Abs(head - tail) <= 1e-6f) + continue; + + BuildRibbon(req, r, tail, head, chunks, pieces); + } + + return pieces; + } + + /// + /// 面片沿路径的一段连续进度区间(起点进度、终点进度、起终点的参数 t) + /// + private struct Span + { + public float fromProgress; + public float toProgress; + public float fromParam; + public float toParam; + public float ArcLength; + } + + private static void BuildRibbon(Request req, int ribbonIndex, float tail, float head, + int chunks, List output) + { + var spans = ResolveSpans(req, tail, head); + if (spans.Count == 0) + return; + + float segments = req.segmentCount; + + Color32 tint = (req.ribbonTints != null && ribbonIndex < req.ribbonTints.Length) + ? req.ribbonTints[ribbonIndex] + : new Color32(255, 255, 255, 255); + bool global = req.uvSpace == SplineUvProgressSpace.SplineGlobal; + + int firstOfRibbon = output.Count; + // 与本次新增的 piece 一一对应的实测弧长:RibbonLocal 的 uv 分母就用这一份加总 + var arcs = new List(); + + foreach (var span in spans) + { + if (span.ArcLength <= 0f) + continue; + + var cuts = CollectCuts(req, span); + int intervalCount = cuts.Count - 1; + float spanWalked = 0f; + + for (int i = 0; i < intervalCount; i++) + { + float a = cuts[i]; + float b = cuts[i + 1]; + float arc = MeasureArc(req, a, b); + + int seg = Mathf.Clamp(Mathf.FloorToInt(a * segments), 0, req.segmentCount - 1); + float localA = Mathf.Clamp01(a * segments - seg); + float localB = Mathf.Clamp01(b * segments - seg); + if (localB - localA <= 1e-6f) + continue; + + Vector2 p0, p1, p2, p3; + GetSegment(req, seg, out p0, out p1, out p2, out p3); + float q0, q1, q2, q3; + GetWidthSegment(req, seg, out q0, out q1, out q2, out q3); + + var piece = new RibbonPiece(); + piece.ribbonIndex = ribbonIndex; + piece.segmentFraction = localB - localA; + + Restrict(p0, p1, p2, p3, localA, localB, + out piece.c0, out piece.c1, out piece.c2, out piece.c3); + Restrict(q0, q1, q2, q3, localA, localB, + out piece.w0, out piece.w1, out piece.w2, out piece.w3); + + piece.startColor = Tint(ColorAt(req, a), tint); + piece.endColor = Tint(ColorAt(req, b), tint); + + // SplineGlobal:落在路径上的绝对进度,环绕时在 1→0 处天然断开。 + // spanWalked 只在这里用 —— 它要的是"在本区间内走了多长"。 + // RibbonLocal 的 uv 留给下面那一趟,用和分子同一份来源的弧长来分配。 + if (global) + { + piece.uvYStart = ProgressAt(span, spanWalked / span.ArcLength); + piece.uvYEnd = ProgressAt(span, (spanWalked + arc) / span.ArcLength); + spanWalked += arc; + output.Add(piece); + continue; + } + + spanWalked += arc; + output.Add(piece); + arcs.Add(arc); + } + } + + if (global) + return; + + // RibbonLocal:整条面片(含环绕的两段)合成一个连续的 0→1。 + // + // 分子分母必须同源。以前分母用 MeasureSpanArc(整个区间一次量完的折线长), + // 分子用逐小块的 MeasureArc(切得更细、量得更长),两者能差 0.2%, + // 于是中间的小块算出 uv > 1,末片又被钉成 1 ⇒ 出现"某片 uv 非增"。 + // 更糟的是原先靠"emit 计数 == 预计块数"来认最后一块:被跳过的退化小块 + // 也已经计过一次数 ⇒ 1 被压到中间某块上,下一块又从 0.6 起重来 —— + // 真机门量到的"跨尾面片 uv 回跳 4 次"就是这么来的。 + // 现在出完片再按这份弧长统一分配,两端自然落在 0 与 1,中间跳过几块都不影响单调。 + // + float totalArc = 0f; + for (int i = 0; i < arcs.Count; i++) + totalArc += arcs[i]; + + if (arcs.Count == 0 || totalArc <= 0f) + { + output.RemoveRange(firstOfRibbon, output.Count - firstOfRibbon); + return; + } + + float cursor = 0f; + for (int i = 0; i < arcs.Count; i++) + { + int pieceIndex = firstOfRibbon + i; + var piece = output[pieceIndex]; + float from = cursor; + cursor += arcs[i]; + + piece.uvYStart = from / totalArc; + piece.uvYEnd = i == arcs.Count - 1 ? 1f : cursor / totalArc; + output[pieceIndex] = piece; + } + } + + /// + /// 把一对 (头部,尾部) 展开成沿路径的区间。 + /// head > tail 是一个区间;循环样条上 head < tail 表示"跨过终点绕回去", + /// 拆成 [tail,1] 与 [0,head] 两个区间,面片仍然连续。非循环样条不做环绕,直接不出片。 + /// + private static List ResolveSpans(Request req, float tail, float head) + { + var spans = new List(req.curveLoop ? 2 : 1); + + if (head > tail) + { + AddSpan(spans, req, tail, head); + return spans; + } + + if (!req.curveLoop) + return spans; + + AddSpan(spans, req, tail, 1f); + AddSpan(spans, req, 0f, head); + return spans; + } + + private static void AddSpan(List spans, Request req, float fromProgress, float toProgress) + { + if (toProgress - fromProgress <= 1e-6f) + return; + + // 退化:校正后端点仍然重合(长度为零的样条会出现),这个区间不出 + float fromParam = Mathf.Clamp01(req.progressToParametric(fromProgress)); + float toParam = Mathf.Clamp01(req.progressToParametric(toProgress)); + if (toParam - fromParam <= 1e-6f) + return; + + var span = new Span + { + fromProgress = fromProgress, + toProgress = toProgress, + fromParam = fromParam, + toParam = toParam, + }; + span.ArcLength = MeasureSpanArc(req, fromParam, toParam); + spans.Add(span); + } + + /// + /// 跨段的区间弧长:先按样条段边界切开,再逐段量。 + /// MeasureArc 只能量"同一段内"的两点,直接拿来量整段会静默少算。 + /// + private static float MeasureSpanArc(Request req, float a, float b) + { + float segments = req.segmentCount; + float sum = 0f; + float cursor = a; + + while (cursor < b - 1e-6f) + { + int seg = Mathf.Clamp(Mathf.FloorToInt(cursor * segments), 0, req.segmentCount - 1); + float nextBoundary = (seg + 1f) / segments; + float stop = Mathf.Min(b, nextBoundary); + sum += MeasureArc(req, cursor, stop); + cursor = stop; + } + return sum; + } + + /// 区间内按弧长占比折算出该处的进度值 + private static float ProgressAt(Span span, float fraction) + { + return Mathf.Clamp01(span.fromProgress + (span.toProgress - span.fromProgress) * fraction); + } + + /// + /// 切分点:等进度网格 + 所有落在区间内的样条段边界,去重后升序。 + /// 有了段边界,每个小块必然完整落在同一段贝塞尔里。 + /// + private static List CollectCuts(Request req, Span span) + { + int chunks = Mathf.Max(1, req.chunksPerRibbon); + var cuts = new List(chunks + req.segmentCount + 2) { span.fromParam, span.toParam }; + + for (int s = 1; s < req.segmentCount; s++) + { + float boundary = (float)s / req.segmentCount; + if (boundary > span.fromParam && boundary < span.toParam) + cuts.Add(boundary); + } + + for (int k = 1; k < chunks; k++) + { + float progress = span.fromProgress + + (span.toProgress - span.fromProgress) * k / chunks; + cuts.Add(Mathf.Clamp01(req.progressToParametric(progress))); + } + + cuts.Sort(); + + var unique = new List(cuts.Count); + for (int i = 0; i < cuts.Count; i++) + { + if (unique.Count > 0 && cuts[i] - unique[unique.Count - 1] <= 1e-6f) + continue; + unique.Add(cuts[i]); + } + return unique; + } + + /// 段 seg 的四个控制点:本点锚、本点出手柄、下一点入手柄、下一点锚 + private static void GetSegment(Request req, int seg, + out Vector2 p0, out Vector2 p1, out Vector2 p2, out Vector2 p3) + { + int next = NextPoint(req, seg); + p0 = req.anchors[seg]; + p1 = req.outHandles[seg]; + p2 = req.inHandles[next]; + p3 = req.anchors[next]; + } + + /// + /// 宽度也走同一条贝塞尔:本段两端取本点宽、下段两端取下点宽。 + /// 与几何用同一个 [t0,t1] 限制,带宽过渡才不会和形状错位。 + /// + private static void GetWidthSegment(Request req, int seg, + out float w0, out float w1, out float w2, out float w3) + { + int next = NextPoint(req, seg); + w0 = WidthAt(req, seg); + w1 = WidthAt(req, seg); + w2 = WidthAt(req, next); + w3 = WidthAt(req, next); + } + + private static int NextPoint(Request req, int seg) + { + return req.curveLoop + ? PositiveMod(seg + 1, req.pointCount) + : Mathf.Min(seg + 1, req.pointCount - 1); + } + + private static float WidthAt(Request req, int index) + { + if (req.pointWidths == null || req.pointWidths.Length == 0) + return 0f; + return req.pointWidths[Mathf.Clamp(index, 0, req.pointWidths.Length - 1)]; + } + + /// 按渐层取色;索引越界一律夹紧,绝不裸索引(渐层列表可能比点少) + private static Color ColorAt(Request req, float parametric) + { + if (req.pointColors == null || req.pointColors.Length == 0) + return Color.white; + + int count = req.pointColors.Length; + // 与 XericRectSpline.GetDeltaT 同一套映射(那个是 internal,Release 侧够不到), + // 所以取色和几何形状保持同相 + float usable = req.curveLoop ? count : Mathf.Max(1, count - 1); + float scaled = Mathf.Clamp01(parametric) * usable; + int index = Mathf.FloorToInt(scaled); + float dt = Mathf.Clamp01(scaled - index); + + int a = req.curveLoop ? PositiveMod(index, count) : Mathf.Clamp(index, 0, count - 1); + int b = req.curveLoop ? PositiveMod(index + 1, count) : Mathf.Clamp(index + 1, 0, count - 1); + return Color.Lerp(req.pointColors[a], req.pointColors[b], dt); + } + + private static Color32 Tint(Color baseColor, Color32 tint) + { + var t = (Color)tint; + var r = new Color(baseColor.r * t.r, baseColor.g * t.g, + baseColor.b * t.b, baseColor.a * t.a); + return r; + } + + private static int PositiveMod(int value, int modulus) + { + if (modulus <= 0) + return 0; + return ((value % modulus) + modulus) % modulus; + } + + // ---------------------------------------------------------------- 贝塞尔裁剪 + + /// + /// 三次贝塞尔在 [t0,t1] 上的精确限制: + /// Q0=B(t0),Q1=B(t0)+(t1-t0)/3·B'(t0),Q2=B(t1)-(t1-t0)/3·B'(t1),Q3=B(t1)。 + /// + internal static void Restrict(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, + float t0, float t1, out Vector2 q0, out Vector2 q1, out Vector2 q2, out Vector2 q3) + { + float span = t1 - t0; + q0 = Eval(p0, p1, p2, p3, t0); + q1 = q0 + Derivative(p0, p1, p2, p3, t0) * (span / 3f); + q3 = Eval(p0, p1, p2, p3, t1); + q2 = q3 - Derivative(p0, p1, p2, p3, t1) * (span / 3f); + } + + internal static void Restrict(float p0, float p1, float p2, float p3, + float t0, float t1, out float q0, out float q1, out float q2, out float q3) + { + float span = t1 - t0; + q0 = Eval(p0, p1, p2, p3, t0); + q1 = q0 + Derivative(p0, p1, p2, p3, t0) * (span / 3f); + q3 = Eval(p0, p1, p2, p3, t1); + q2 = q3 - Derivative(p0, p1, p2, p3, t1) * (span / 3f); + } + + internal static Vector2 Eval(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, float t) + { + var a = Vector2.Lerp(p0, p1, t); + var b = Vector2.Lerp(p1, p2, t); + var c = Vector2.Lerp(p2, p3, t); + var d = Vector2.Lerp(a, b, t); + var e = Vector2.Lerp(b, c, t); + return Vector2.Lerp(d, e, t); + } + + internal static float Eval(float p0, float p1, float p2, float p3, float t) + { + var a = Mathf.Lerp(p0, p1, t); + var b = Mathf.Lerp(p1, p2, t); + var c = Mathf.Lerp(p2, p3, t); + var d = Mathf.Lerp(a, b, t); + var e = Mathf.Lerp(b, c, t); + return Mathf.Lerp(d, e, t); + } + + /// 导数 = 3 × 一阶差分控制点的二次贝塞尔 + internal static Vector2 Derivative(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, float t) + { + var d0 = p1 - p0; + var d1 = p2 - p1; + var d2 = p3 - p2; + var a = Vector2.Lerp(d0, d1, t); + var b = Vector2.Lerp(d1, d2, t); + return Vector2.Lerp(a, b, t) * 3f; + } + + internal static float Derivative(float p0, float p1, float p2, float p3, float t) + { + var d0 = p1 - p0; + var d1 = p2 - p1; + var d2 = p3 - p2; + var a = Mathf.Lerp(d0, d1, t); + var b = Mathf.Lerp(d1, d2, t); + return Mathf.Lerp(a, b, t) * 3f; + } + + /// 小块弧长(折线近似,够 uv 分配用) + private static float MeasureArc(Request req, float a, float b) + { + const int samples = 5; + int seg = Mathf.Clamp(Mathf.FloorToInt(a * req.segmentCount), 0, req.segmentCount - 1); + + Vector2 p0, p1, p2, p3; + GetSegment(req, seg, out p0, out p1, out p2, out p3); + + float la = Mathf.Clamp01(a * req.segmentCount - seg); + float lb = Mathf.Clamp01(b * req.segmentCount - seg); + + var last = Eval(p0, p1, p2, p3, la); + float sum = 0f; + for (int i = 1; i <= samples; i++) + { + var next = Eval(p0, p1, p2, p3, Mathf.Lerp(la, lb, (float)i / samples)); + sum += Vector2.Distance(last, next); + last = next; + } + return sum; + } + } +} diff --git a/Runtime/Generation/Curve/SplineRibbonBuilder.cs.meta b/Runtime/Generation/Curve/SplineRibbonBuilder.cs.meta new file mode 100644 index 0000000..c3043eb --- /dev/null +++ b/Runtime/Generation/Curve/SplineRibbonBuilder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2849f073cdc1b8028779b7b6c5fedfe3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Generation/Curve/UISplineCurveRenderer.cs b/Runtime/Generation/Curve/UISplineCurveRenderer.cs new file mode 100644 index 0000000..6e2e43f --- /dev/null +++ b/Runtime/Generation/Curve/UISplineCurveRenderer.cs @@ -0,0 +1,472 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +using XericLibrary.Runtime.XericComponent; + +#if UNITY_EDITOR +using UnityEditor; +#endif + +namespace XericLibrary.Runtime.UIGraph +{ + /// + /// 由界面样条(XericRectSplinePath)驱动的曲线绘制组件。 + /// + /// 一条样条上可以同时铺多条"面片",每条由一对 (头部, 尾部) 进度值决定覆盖范围, + /// 于是 {1,0} 是铺满整条的底轨,{0.6f,0} 就是填到 60% 的进度条本体。 + /// 进度默认经样条烘焙后的弧长重映射校正,画面上的推进长度才和进度值成正比; + /// 面片上的 uv0.y 被重映射成整条面片的 0→1,材质按 UV 就能做流动/遮罩类效果。 + /// + /// + [AddComponentMenu("Xeric Library/UI/UISplineCurveRenderer", 17)] + public class UISplineCurveRenderer : CurveCacheRendererBase + { + #region 字段属性 + + /// 驱动本曲线的界面样条 + public XericRectSplinePath splinePath; + + /// + /// 面片对:偶数下标=头部、奇数下标=尾部,取值 [0,1]。 + /// 例:{1,0} 一条铺满;{1,0, 0.6f,0} 两条=底轨 + 60% 填充。 + /// + public float[] headTailPairs = { 1f, 0f }; + + /// 可空;与面片同序时逐条叠乘到样条颜色上(底轨压暗、填充提亮) + public Color32[] ribbonTints; + + /// 进度值的含义:直接按参数,还是先经烘焙做弧长校正 + public SplineProgressMapping progressMapping = SplineProgressMapping.BakedDistance; + + /// 烘焙采样数,越大校正越细;同时压住库侧 1/(N+1) 的系统偏差 + [Range(8, 4096)] + public int bakeSubdivide = 256; + + /// + /// 什么时候做弧长烘焙。默认 OnEditEnd:几何停止变化后补烘一次。 + /// 烘焙是 O(subdivide) 的采样,放在每帧会把编辑器拖到卡。 + /// + public SplineBakeTrigger bakeTrigger = SplineBakeTrigger.OnEditEnd; + + /// 已经烘过几次(排查用:编辑器里拖动一整轮应该只加 1) + public int BakeRunCount => m_BakeRunCount; + + /// 面片上 uv0.y 代表条内进度还是全样条进度 + public SplineUvProgressSpace uvProgressSpace = SplineUvProgressSpace.RibbonLocal; + + /// 每条面片至少切成几块,决定 uv 与视觉距离对齐的精度 + [Range(1, 64)] + public int chunksPerRibbon = 8; + + /// 基准带宽(样条所在矩形的局部像素) + [Min(0.01f)] + public float curveWidth = 10f; + + /// 带宽来源:常量,还是乘上样条的缩放渐层 Y + public SplineWidthSource widthSource = SplineWidthSource.ScaleGradientY; + + /// 单段贝塞尔的细分段数 + [Range(2, 128)] + public int tessellationSegments = 32; + + /// 样条颜色渐层全透明时的兜底颜色 + public Color fallbackColor = Color.white; + + /// + /// 推给材质 _MainTex 的贴图。 + /// Canvas 是用 MaterialPropertyBlock 把 Graphic.mainTexture 顶到 _MainTex 上的, + /// 所以必须走这个口子交出去;只改 material 的贴图会被默认白图覆盖掉。 + /// + public Texture progressTexture; + + /// SetProgress 作用在哪条面片的头部;-1 表示不指定 + public int fillRibbonIndex = 1; + + /// 每帧跟随样条变化重建(关掉后只能靠 RebuildCurve 手动触发) + public bool followSplineChanges = true; + + /// 与曲线缓存同序的 uv 区间,x=起点 y=终点 + private readonly List m_PieceUv = new List(); + + private bool m_NeedUvPatch; + private int m_LastBakeSignature = int.MinValue; + private bool m_BakePending; + private int m_BakeRunCount; + + #endregion + + #region 生命周期 + + public override Texture mainTexture + => progressTexture != null ? progressTexture : base.mainTexture; + + protected override void OnEnable() + { + base.OnEnable(); + RebuildCurve(); + } + +#if UNITY_EDITOR + protected override void OnValidate() + { + base.OnValidate(); + if (curveWidth < 0.01f) curveWidth = 0.01f; + if (tessellationSegments < 2) tessellationSegments = 2; + if (followSplineChanges && isActiveAndEnabled) + { + EditorApplication.delayCall += () => + { + if (this != null && isActiveAndEnabled) + RebuildCurve(); + }; + } + } +#endif + + protected override void LateUpdate() + { + if (followSplineChanges) + RebuildCurve(); + + base.LateUpdate(); + + // 只能在"刚刚重建过缓存"之后改写一次: + // 改写后 uv0.y 就不再是生成器写的段内 t,重复改写会把映射套两遍 + if (m_NeedUvPatch) + { + m_NeedUvPatch = false; + ApplyRibbonUv(); + } + } + + #endregion + + #region API + + /// + /// 把指定面片的头部推到 value(进度条前进就调这个) + /// + public void SetProgress(float value) + { + int index = fillRibbonIndex >= 0 ? fillRibbonIndex : 0; + if (headTailPairs == null || headTailPairs.Length < (index + 1) * 2) + return; + + headTailPairs[index * 2] = Mathf.Clamp01(value); + RebuildCurve(); + } + + /// + /// 读取指定面片的头部进度 + /// + public float GetProgress() + { + int index = fillRibbonIndex >= 0 ? fillRibbonIndex : 0; + if (headTailPairs == null || headTailPairs.Length < (index + 1) * 2) + return 0f; + return headTailPairs[index * 2]; + } + + /// + /// 按当前样条与面片配置重建曲线 + /// + public void RebuildCurve() + { + ClearAll(); + m_PieceUv.Clear(); + m_NeedUvPatch = false; + + if (splinePath == null || rectTransform == null) + return; + + var spline = splinePath.xericSpline; + if (spline == null || spline.Count < 2) + return; + + // 锚点→世界的换算依赖矩形角点 + splinePath.UpdateRectDepend(); + var rect = rectTransform.rect; + if (Mathf.Abs(rect.width) < 1e-6f || Mathf.Abs(rect.height) < 1e-6f) + return; + + int pointCount = spline.Count; + var anchors = new Vector2[pointCount]; + var outHandles = new Vector2[pointCount]; + var inHandles = new Vector2[pointCount]; + var colors = new Color[pointCount]; + var widths = new float[pointCount]; + + int signature = pointCount; + for (int i = 0; i < pointCount; i++) + { + anchors[i] = (Vector2)rectTransform.InverseTransformPoint(splinePath.GetPointPosition(i)); + outHandles[i] = (Vector2)rectTransform.InverseTransformPoint(splinePath.GetEndHandlePosition(i)); + inHandles[i] = (Vector2)rectTransform.InverseTransformPoint(splinePath.GetStartHandlePosition(i)); + colors[i] = ColorAt(spline, i); + widths[i] = WidthAt(spline, i); + + unchecked + { + signature = signature * 31 + anchors[i].GetHashCode() + + outHandles[i].GetHashCode() * 7 + inHandles[i].GetHashCode() * 13; + } + } + // 非均匀缩放矩形会改变世界空间弧长的比例,签名要把矩形尺寸也算进去 + unchecked + { + signature = signature * 31 + rect.width.GetHashCode() + rect.height.GetHashCode() * 3; + } + + var request = new SplineRibbonBuilder.Request + { + anchors = anchors, + outHandles = outHandles, + inHandles = inHandles, + pointColors = colors, + pointWidths = widths, + pointCount = pointCount, + segmentCount = spline.useablePointCount, + curveLoop = spline.CurveLoop, + headTailPairs = headTailPairs, + ribbonTints = ribbonTints, + uvSpace = uvProgressSpace, + chunksPerRibbon = chunksPerRibbon, + progressToParametric = BuildMapping(spline, signature), + }; + + var pieces = SplineRibbonBuilder.Build(request); + if (pieces.Count == 0) + return; + + var ctrl = new Vector2[4]; + var pieceWidths = new float[4]; + + foreach (var piece in pieces) + { + ctrl[0] = piece.c0; ctrl[1] = piece.c1; ctrl[2] = piece.c2; ctrl[3] = piece.c3; + pieceWidths[0] = piece.w0; pieceWidths[1] = piece.w1; + pieceWidths[2] = piece.w2; pieceWidths[3] = piece.w3; + + var entry = new CurveCacheEntry + { + startColor = piece.startColor, + endColor = piece.endColor, + tessellationSegments = PieceTessellation(piece), + }; + + AddCurve(entry, (Vector2[])ctrl.Clone(), (float[])pieceWidths.Clone()); + m_PieceUv.Add(new Vector2(piece.uvYStart, piece.uvYEnd)); + } + + m_NeedUvPatch = m_PieceUv.Count > 0; + } + + #endregion + + #region 取数 + + /// + /// 造出"进度 → 参数 t"的映射。校正走样条烘焙的弧长重映射, + /// 库侧要求先烘焙且长度不足时会抛,所以这里自己把关并回落。 + /// + private Func BuildMapping(XericRectSpline spline, int signature) + { + if (progressMapping != SplineProgressMapping.BakedDistance) + return ParametricIdentity; + + if (!EnsureBaked(spline, signature)) + return ParametricIdentity; + + return ParametricFromBakedDistance; + } + + private float ParametricIdentity(float progress) => progress; + + /// + /// 进度→参数 t。两端是边界真值:进度 0 就是曲线起点、1 就是终点, + /// 所以先把两端钉死再走重映射 —— 库里的弧长重映射原本按"采样点数"而不是"段数" + /// 做分母,进度 1 会落在 (N-1)/N,{1,0} 的面片就永远短一小截贴不到末尾。 + /// 库侧已经修了分母,这里钉端点既是兜底也让旧 DLL 下也能立刻闭合。 + /// + private float ParametricFromBakedDistance(float progress) + { + if (progress <= 0f) return 0f; + if (progress >= 1f) return 1f; + return Mathf.Clamp01(splinePath.RemappingTimeValue(progress)); + } + + /// + /// 按 决定这一趟烘不烘,返回 false 表示当前用不了弧长校正。 + /// + /// 烘焙是 O(subdivide) 的采样:以前几何签名一变就烘,编辑器里拖动控制点等于每帧烘一次。 + /// OnEditEnd 的做法是"几何正在线上变"时先挂起、继续用上一次那份烘焙数据, + /// 等下一趟签名不再变化(编辑停了)才补烘那一次。 + /// + /// + private bool EnsureBaked(XericRectSpline spline, int signature) + { + int target = Mathf.Max(bakeSubdivide, spline.Count); + bool geometryChanged = signature != m_LastBakeSignature; + m_LastBakeSignature = signature; + + switch (bakeTrigger) + { + case SplineBakeTrigger.Manual: + // 一次都不自动烘,等 BakeNow + return HasUsableBake(spline, target); + + case SplineBakeTrigger.EveryRebuild: + if (!geometryChanged && HasUsableBake(spline, target)) + return true; + return RunBake(spline, target); + + default: + if (geometryChanged) + { + // 几何正在线上变:这一趟不烘,也**不用**上一份烘焙数据。 + // 结构大改(清点重铺、切循环)后那份表对应的已经不是这条曲线了, + // 拿它做校正会把面片排到错处 —— 宁可这一趟退回参数映射, + // 等编辑停下来的下一趟烘完再恢复校正。 + m_BakePending = true; + return false; + } + if (!m_BakePending) + return HasUsableBake(spline, target); + + m_BakePending = false; + return RunBake(spline, target); + } + } + + /// 烘焙数据存在且长度够用(旧数据只要长度够就继续用,别再烘一次) + private static bool HasUsableBake(XericRectSpline spline, int target) + => spline.BakeCount >= target; + + private bool RunBake(XericRectSpline spline, int target) + { + splinePath.BakePoint(target); + m_BakeRunCount++; + return spline.BakeCount >= target; + } + + /// + /// 手动烘一次(bakeTrigger 为 Manual 时用这个,或改完曲线想立刻精确时按)。 + /// 烘完直接重建,按下就能看见贴合结果。 + /// + public void BakeNow() + { + if (splinePath == null || splinePath.xericSpline == null) + return; + + var spline = splinePath.xericSpline; + RunBake(spline, Mathf.Max(bakeSubdivide, spline.Count)); + m_BakePending = false; + RebuildCurve(); + } + + private Color ColorAt(XericRectSpline spline, int index) + { + var gradient = spline.ColorGradient; + if (gradient == null || index < 0 || index >= gradient.Count) + return fallbackColor; + + var color = gradient[index]; + return color.a <= 0f ? fallbackColor : color; + } + + private float WidthAt(XericRectSpline spline, int index) + { + switch (widthSource) + { + case SplineWidthSource.ScaleGradientY: + return curveWidth * Mathf.Abs(GradientAt(spline.ScaleGradient, index, Vector3.one).y); + case SplineWidthSource.FloatGradient: + // 额外参数本来就是"每点多大"的语义时用它最直观; + // 注意它默认全 0,不填就什么都画不出来 + return curveWidth * Mathf.Max(0f, GradientAt(spline.FloatGradient, index, 1f)); + default: + return curveWidth; + } + } + + /// 渐层可能比点数短(在 Inspector 里直接改 ControlPoint 长度就会这样),一律夹紧 + private static T GradientAt(SplineGradientBase gradient, int index, T fallback) + { + if (gradient == null || index < 0 || index >= gradient.Count) + return fallback; + return gradient[index]; + } + + private int PieceTessellation(RibbonPiece piece) + { + int tess = Mathf.RoundToInt(tessellationSegments * Mathf.Clamp01(piece.segmentFraction)); + return Mathf.Clamp(tess, 2, 512); + } + + #endregion + + #region uv 重写 + + /// + /// 把生成器写在每个小块里的"段内 t"重映射成整条面片的 0→1。 + /// 默认材质只用到 uv0.x(边缘羽化),uv0.y 留给换上去的材质消费。 + /// + private void ApplyRibbonUv() + { + if (m_PieceUv.Count == 0) + return; + + for (int i = 0; i < m_Curves.Count && i < m_PieceUv.Count; i++) + { + var curve = m_Curves[i]; + var range = m_PieceUv[i]; + int end = curve.startVertexIndex + curve.vertexCount; + if (end > m_MeshVertexCache.Count) + end = m_MeshVertexCache.Count; + + for (int v = curve.startVertexIndex; v < end; v++) + { + var vert = m_MeshVertexCache[v]; + vert.uv0.y = Mathf.LerpUnclamped(range.x, range.y, vert.uv0.y); + m_MeshVertexCache[v] = vert; + } + } + + SetVerticesDirty(); + } + + #endregion + } + + /// 进度值如何换算成曲线参数 + public enum SplineProgressMapping + { + /// 进度直接当参数 t:段与段的视觉长度不等 + Parametric, + /// 进度按烘焙后的弧长重映射:画面上推进的长度和进度成正比 + BakedDistance, + } + + /// 弧长烘焙的触发时机(校正的数据源就是这份烘焙数据) + public enum SplineBakeTrigger + { + /// 从不自动烘,只认 BakeNow / Inspector 上的烘焙按钮 + Manual, + /// 几何停止变化后补烘一次:编辑器里拖动控制点不会逐帧烘 + OnEditEnd, + /// 几何一变就烘(每帧一变烘一次,贵;只在需要立刻精确时用) + EveryRebuild, + } + + /// 带宽来源 + public enum SplineWidthSource + { + /// 恒用 curveWidth + Constant, + /// curveWidth × 该点缩放渐层的 Y(渐层默认是 1,不填也等价于常量) + ScaleGradientY, + /// curveWidth × 该点的额外参数。默认全 0,不填什么都画不出来 + FloatGradient, + } +} diff --git a/Runtime/Generation/Curve/UISplineCurveRenderer.cs.meta b/Runtime/Generation/Curve/UISplineCurveRenderer.cs.meta new file mode 100644 index 0000000..7a1da9d --- /dev/null +++ b/Runtime/Generation/Curve/UISplineCurveRenderer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 91cbc8bc694b85a8f15fefce8d3a72b1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Generation/Curve/UISplineProgressDriver.cs b/Runtime/Generation/Curve/UISplineProgressDriver.cs new file mode 100644 index 0000000..e29ea89 --- /dev/null +++ b/Runtime/Generation/Curve/UISplineProgressDriver.cs @@ -0,0 +1,65 @@ +using UnityEngine; + +namespace XericLibrary.Runtime.UIGraph +{ + /// + /// 曲线进度条的播放驱动器:把时间推成 UISplineCurveRenderer 的进度。 + /// 只负责"进度从哪来",几何与着色全在渲染器那一侧。 + /// + [AddComponentMenu("Xeric Library/UI/UISplineProgressDriver", 18)] + [RequireComponent(typeof(UISplineCurveRenderer))] + public class UISplineProgressDriver : MonoBehaviour + { + /// 自动推进(关掉就只认手动 SetProgress) + public bool autoDrive = true; + + /// 从 0 走到 1 所需秒数 + [Min(0.01f)] + public float duration = 2.5f; + + /// 用 scaledTime,随引擎暂停/缩放 + public bool useUnscaledTime = false; + + /// 走到尽头后往返还是从头再来 + public bool pingPong = false; + + /// 起始进度 + [Range(0f, 1f)] + public float startProgress = 0f; + + private UISplineCurveRenderer m_Renderer; + private float m_Elapsed; + + private void Awake() + { + m_Renderer = GetComponent(); + m_Elapsed = startProgress * duration; + if (m_Renderer != null) + m_Renderer.SetProgress(startProgress); + } + + private void Update() + { + if (!autoDrive || m_Renderer == null) + return; + + m_Elapsed += useUnscaledTime ? Time.unscaledDeltaTime : Time.deltaTime; + float cycle = m_Elapsed / duration; + + float progress = pingPong + ? Mathf.PingPong(cycle, 1f) + : cycle - Mathf.Floor(cycle); + + m_Renderer.SetProgress(progress); + } + + /// 手动设进度,同时让自动推进从该处继续 + public void SetProgress(float value) + { + value = Mathf.Clamp01(value); + m_Elapsed = value * duration; + if (m_Renderer != null) + m_Renderer.SetProgress(value); + } + } +} diff --git a/Runtime/Generation/Curve/UISplineProgressDriver.cs.meta b/Runtime/Generation/Curve/UISplineProgressDriver.cs.meta new file mode 100644 index 0000000..fad73c9 --- /dev/null +++ b/Runtime/Generation/Curve/UISplineProgressDriver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4f851c7a6653a78879ce665e643f5a28 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/UI/UILineRenderer/Gradient.png b/Runtime/UI/UILineRenderer/Gradient.png new file mode 100644 index 0000000..49d6c75 Binary files /dev/null and b/Runtime/UI/UILineRenderer/Gradient.png differ diff --git a/Runtime/UI/UILineRenderer/Gradient.png.meta b/Runtime/UI/UILineRenderer/Gradient.png.meta new file mode 100644 index 0000000..7e082e3 --- /dev/null +++ b/Runtime/UI/UILineRenderer/Gradient.png.meta @@ -0,0 +1,127 @@ +fileFormatVersion: 2 +guid: 65b82257fc9da6b4ba046b02555dd0d6 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/UI/UILineRenderer/LICENSE b/Runtime/UI/UILineRenderer/LICENSE new file mode 100644 index 0000000..c32dd18 --- /dev/null +++ b/Runtime/UI/UILineRenderer/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to \ No newline at end of file diff --git a/Runtime/MathNet.Numerics.xml.meta b/Runtime/UI/UILineRenderer/LICENSE.meta similarity index 62% rename from Runtime/MathNet.Numerics.xml.meta rename to Runtime/UI/UILineRenderer/LICENSE.meta index 73c59d2..1cee071 100644 --- a/Runtime/MathNet.Numerics.xml.meta +++ b/Runtime/UI/UILineRenderer/LICENSE.meta @@ -1,6 +1,6 @@ fileFormatVersion: 2 -guid: 9abe5f919a1840a4dace5de52d721a06 -TextScriptImporter: +guid: affe7d56dc01a48439aedc888552bf47 +DefaultImporter: externalObjects: {} userData: assetBundleName: diff --git a/Runtime/UI/UILineRenderer/Sample.unity b/Runtime/UI/UILineRenderer/Sample.unity new file mode 100644 index 0000000..2e8bbd0 --- /dev/null +++ b/Runtime/UI/UILineRenderer/Sample.unity @@ -0,0 +1,638 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 705507994} + m_IndirectSpecularColor: {r: 0.44657874, g: 0.49641275, b: 0.5748172, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 500 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 500 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 2 + m_PVRDenoiserTypeDirect: 0 + m_PVRDenoiserTypeIndirect: 0 + m_PVRDenoiserTypeAO: 0 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 0 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &246206081 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 246206082} + - component: {fileID: 246206084} + - component: {fileID: 246206083} + m_Layer: 5 + m_Name: LineRenderer + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &246206082 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 246206081} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1.5, y: 1.5, z: 1.5} + m_ConstrainProportionsScale: 1 + m_Children: [] + m_Father: {fileID: 387643621} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 1000, y: 300} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &246206083 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 246206081} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5b33b2e663e78774c9f0c9af55018725, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 10754, guid: 0000000000000000f000000000000000, type: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + points: + - {x: 0, y: 0} + - {x: 58.7, y: 138.4} + - {x: 125.1, y: 80.6} + - {x: 238.9, y: 300} + - {x: 354, y: 100} + - {x: 512.2, y: 70} + - {x: 716.1, y: 192.5} + - {x: 938.8, y: 3.85} + - {x: 1000, y: 133} + thickness: 20 + center: 1 +--- !u!222 &246206084 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 246206081} + m_CullTransparentMesh: 1 +--- !u!1 &387643617 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 387643621} + - component: {fileID: 387643620} + - component: {fileID: 387643619} + - component: {fileID: 387643618} + m_Layer: 5 + m_Name: Canvas + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &387643618 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 387643617} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreReversedGraphics: 1 + m_BlockingObjects: 0 + m_BlockingMask: + serializedVersion: 2 + m_Bits: 4294967295 +--- !u!114 &387643619 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 387643617} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3} + m_Name: + m_EditorClassIdentifier: + m_UiScaleMode: 1 + m_ReferencePixelsPerUnit: 100 + m_ScaleFactor: 1 + m_ReferenceResolution: {x: 1920, y: 1080} + m_ScreenMatchMode: 0 + m_MatchWidthOrHeight: 0 + m_PhysicalUnit: 3 + m_FallbackScreenDPI: 96 + m_DefaultSpriteDPI: 96 + m_DynamicPixelsPerUnit: 1 + m_PresetInfoIsWorld: 0 +--- !u!223 &387643620 +Canvas: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 387643617} + m_Enabled: 1 + serializedVersion: 3 + m_RenderMode: 0 + m_Camera: {fileID: 0} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_VertexColorAlwaysGammaSpace: 0 + m_AdditionalShaderChannelsFlag: 0 + m_UpdateRectTransformForStandalone: 0 + m_SortingLayerID: 0 + m_SortingOrder: 0 + m_TargetDisplay: 0 +--- !u!224 &387643621 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 387643617} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0, y: 0, z: 0} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1231462426} + - {fileID: 246206082} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0, y: 0} +--- !u!1 &705507993 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 705507995} + - component: {fileID: 705507994} + m_Layer: 0 + m_Name: Directional Light + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!108 &705507994 +Light: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 705507993} + m_Enabled: 1 + serializedVersion: 10 + m_Type: 1 + m_Shape: 0 + m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1} + m_Intensity: 1 + m_Range: 10 + m_SpotAngle: 30 + m_InnerSpotAngle: 21.80208 + m_CookieSize: 10 + m_Shadows: + m_Type: 2 + m_Resolution: -1 + m_CustomResolution: -1 + m_Strength: 1 + m_Bias: 0.05 + m_NormalBias: 0.4 + m_NearPlane: 0.2 + m_CullingMatrixOverride: + e00: 1 + e01: 0 + e02: 0 + e03: 0 + e10: 0 + e11: 1 + e12: 0 + e13: 0 + e20: 0 + e21: 0 + e22: 1 + e23: 0 + e30: 0 + e31: 0 + e32: 0 + e33: 1 + m_UseCullingMatrixOverride: 0 + m_Cookie: {fileID: 0} + m_DrawHalo: 0 + m_Flare: {fileID: 0} + m_RenderMode: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingLayerMask: 1 + m_Lightmapping: 1 + m_LightShadowCasterMode: 0 + m_AreaSize: {x: 1, y: 1} + m_BounceIntensity: 1 + m_ColorTemperature: 6570 + m_UseColorTemperature: 0 + m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} + m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 + m_ShadowRadius: 0 + m_ShadowAngle: 0 +--- !u!4 &705507995 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 705507993} + serializedVersion: 2 + m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261} + m_LocalPosition: {x: 0, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0} +--- !u!1 &963194225 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 963194228} + - component: {fileID: 963194227} + - component: {fileID: 963194226} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &963194226 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 963194225} + m_Enabled: 1 +--- !u!20 &963194227 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 963194225} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 0 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &963194228 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 963194225} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1231462425 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1231462426} + - component: {fileID: 1231462428} + - component: {fileID: 1231462427} + m_Layer: 5 + m_Name: Background + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1231462426 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1231462425} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 1 + m_Children: [] + m_Father: {fileID: 387643621} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1231462427 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1231462425} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 21300000, guid: 980e274b7ed2de945bdefcc1d0423472, type: 3} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &1231462428 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1231462425} + m_CullTransparentMesh: 1 +--- !u!1 &1721653935 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1721653938} + - component: {fileID: 1721653937} + - component: {fileID: 1721653936} + m_Layer: 0 + m_Name: EventSystem + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1721653936 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1721653935} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3} + m_Name: + m_EditorClassIdentifier: + m_SendPointerHoverToParent: 1 + m_HorizontalAxis: Horizontal + m_VerticalAxis: Vertical + m_SubmitButton: Submit + m_CancelButton: Cancel + m_InputActionsPerSecond: 10 + m_RepeatDelay: 0.5 + m_ForceModuleActive: 0 +--- !u!114 &1721653937 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1721653935} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_FirstSelected: {fileID: 0} + m_sendNavigationEvents: 1 + m_DragThreshold: 10 +--- !u!4 &1721653938 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1721653935} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 963194228} + - {fileID: 705507995} + - {fileID: 387643621} + - {fileID: 1721653938} diff --git a/Runtime/UI/UILineRenderer/Sample.unity.meta b/Runtime/UI/UILineRenderer/Sample.unity.meta new file mode 100644 index 0000000..a6579e7 --- /dev/null +++ b/Runtime/UI/UILineRenderer/Sample.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: eb3b7561320c3df409493f5b073278f0 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/UI/UILineRenderer/XericSimpleUILineRenderer.cs b/Runtime/UI/UILineRenderer/XericSimpleUILineRenderer.cs new file mode 100644 index 0000000..5a9da5b --- /dev/null +++ b/Runtime/UI/UILineRenderer/XericSimpleUILineRenderer.cs @@ -0,0 +1,91 @@ +using UnityEngine; +using UnityEngine.UI; + +namespace Radishmouse +{ + [RequireComponent(typeof(CanvasRenderer))] + public class XericSimpleUILineRenderer : MaskableGraphic + { + public Vector2[] points; + + public float thickness = 10f; + public bool center = true; + + protected override void OnPopulateMesh(VertexHelper vh) + { + vh.Clear(); + + if (points.Length < 2) + return; + + for (int i = 0; i < points.Length-1; i++) + { + // Create a line segment between the next two points + CreateLineSegment(points[i], points[i+1], vh); + + int index = i * 5; + + // Add the line segment to the triangles array + vh.AddTriangle(index, index+1, index+3); + vh.AddTriangle(index+3, index+2, index); + + // These two triangles create the beveled edges + // between line segments using the end point of + // the last line segment and the start points of this one + if (i != 0) + { + vh.AddTriangle(index, index-1, index-3); + vh.AddTriangle(index+1, index-1, index-2); + } + } + } + + /// + /// Creates a rect from two points that acts as a line segment + /// + /// The starting point of the segment + /// The endint point of the segment + /// The vertex helper that the segment is added to + private void CreateLineSegment(Vector3 point1, Vector3 point2, VertexHelper vh) + { + Vector3 offset = center ? (rectTransform.sizeDelta / 2) : Vector2.zero; + + // Create vertex template + UIVertex vertex = UIVertex.simpleVert; + vertex.color = color; + + // Create the start of the segment + Quaternion point1Rotation = Quaternion.Euler(0, 0, RotatePointTowards(point1, point2) + 90); + vertex.position = point1Rotation * new Vector3(-thickness / 2, 0); + vertex.position += point1 - offset; + vh.AddVert(vertex); + vertex.position = point1Rotation * new Vector3(thickness / 2, 0); + vertex.position += point1 - offset; + vh.AddVert(vertex); + + // Create the end of the segment + Quaternion point2Rotation = Quaternion.Euler(0, 0, RotatePointTowards(point2, point1) - 90); + vertex.position = point2Rotation * new Vector3(-thickness / 2, 0); + vertex.position += point2 - offset; + vh.AddVert(vertex); + vertex.position = point2Rotation * new Vector3(thickness / 2, 0); + vertex.position += point2 - offset; + vh.AddVert(vertex); + + // Also add the end point + vertex.position = point2 - offset; + vh.AddVert(vertex); + } + + /// + /// Gets the angle that a vertex needs to rotate to face target vertex + /// + /// The vertex being rotated + /// The vertex to rotate towards + /// The angle required to rotate vertex towards target + private float RotatePointTowards(Vector2 vertex, Vector2 target) + { + return (float)(Mathf.Atan2(target.y - vertex.y, target.x - vertex.x) * (180 / Mathf.PI)); + } + } +} diff --git a/Runtime/UI/UILineRenderer/XericSimpleUILineRenderer.cs.meta b/Runtime/UI/UILineRenderer/XericSimpleUILineRenderer.cs.meta new file mode 100644 index 0000000..514a759 --- /dev/null +++ b/Runtime/UI/UILineRenderer/XericSimpleUILineRenderer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3e20d14972d02c04eba084112d510af7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XericCecil.dll b/Runtime/XericCecil.dll index 163e664..a214ccd 100644 Binary files a/Runtime/XericCecil.dll and b/Runtime/XericCecil.dll differ diff --git a/Runtime/XericLibrary.dll b/Runtime/XericLibrary.dll index 8a35a92..702b46f 100644 Binary files a/Runtime/XericLibrary.dll and b/Runtime/XericLibrary.dll differ diff --git a/package.json b/package.json index e7e62e4..274dc24 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "com.lrss3.deconstruction", "displayName": "Xeric Library", - "version": "0.6.4", + "version": "0.6.5", "unity": "2021.3", "description": "\u6b22\u8fce\u4f7f\u7528Xeric Library, \u8fd9\u662f\u4e00\u4e2a\u4e13\u6ce8\u4ee3\u7801\u7684\u6269\u5c55\u5e93 \r\n\r\n * \u4e13\u6ce8Unity\u6210\u5458\u8bed\u6cd5, \u6269\u5c55\u591a\u79cd\u57fa\u672c\u7c7b\u578b\u7684\u6570\u636e\u7ed3\u6784\u7684Linq\u8bed\u6cd5/\u8bed\u6cd5\u7cd6, \u63d0\u4f9b\uff1a \u51fd\u6570\u5e73\u6ed1\u3001 \u591a\u9879\u5f0f\u8ba1\u7b97\u3001 \u6743\u91cd\u62df\u5408\u3001 \u8fc7\u7a0b\u5206\u7ea7\u3001 \u5de5\u4e1a\u63a7\u5236\u3001 \u66f2\u7ebf\u7ed8\u5236\u3001 \u8def\u5f84\u8bbe\u7f6e\u3001 \u5bf9\u8c61\u63a7\u5236\u3001 \u8fed\u4ee3\u6269\u5c55\u3001 \u7a7a\u95f4\u53d8\u6362\u3001 \u72ec\u7279\u7ed3\u6784\u3001 \u6587\u672c\u683c\u5f0f\u3001 \u7c7b\u578b\u8f6c\u6362\u3001 \u673a\u5668\u7f16\u7801\u3001 \u5f00\u53d1\u8c03\u8bd5\u3001 \u52a8\u6001\u751f\u6210\u3001 \u51e0\u4f55\u521b\u5efa\u3001 \u5feb\u901f\u6c60\u5316\u3001 \u5bfc\u822a\u5bfb\u8def\u3001 \u9694\u79bb\u63a7\u5236\u3001 \u9694\u79bb\u8f93\u5165\u3001 \u8bed\u4e49\u5316\u59d4\u6258\u3001 \u53cd\u5c04\u8d85\u9a70\u3001 \u6570\u5b66\u5e38\u6570\u3001 \u5355\u4f4d\u6362\u7b97\u3001 \u6392\u5e8f\u7b97\u6cd5\u3001 \u7a0b\u5e8f\u8c03\u7528\u3001 \u7f51\u7edc\u8fde\u63a5\u7b49\u5feb\u6377\u7528\u6cd5\u3002 \r\n\r\n * \u6269\u5c55\u7279\u6b8a\u7c7b\u578b, \u5305\u62ec\u4e14\u4e0d\u9650\u4e8e: \u591a\u7ef4\u5e03\u5c14\u3001 \u6837\u6761\u66f2\u7ebf\u3001 \u8d85\u7ea7\u5355\u4f8b\u3001 \u591a\u4f8b\u7cfb\u7edf\u3001 \u90bb\u5c45\u7f51\u7edc\u3001 \u56db\u53c9\u6811\u3001 \u5b57\u5178\u6811\u3001 \u8f6f\u5f15\u7528\u5c01\u88c5\u5668\u7b49\u3002 \r\n\r\n * \u6269\u5c55\u5404\u79cd\u5e38\u7528\u811a\u672c: \u6e38\u620f\u4ea4\u4e92\u3001 \u754c\u9762\u9002\u914d\u3001 \u5f31\u6269\u5c55\u3001 \u7ed8\u5236\u5de5\u5382\u3001 SQL\u6269\u5c55\u3002 \r\n\r\n * \u517c\u5bb9\u4e0d\u89c4\u8303\u7a0b\u5e8f: \u9488\u5bf9\u4e0d\u89c4\u8303\u6216\u6beb\u65e0\u8bbe\u8ba1\u89c4\u5219\u53ef\u8a00\u7684\u4ee3\u7801, \u63d0\u4f9b\u4e86\u66f4\u591a\u57fa\u4e8e\u53cd\u5c04, CIL\u7279\u6027\u7b49\u5e95\u5c42\u8bed\u6cd5\u7684\u7a0b\u5e8f\u6269\u5c55, \u4ee5\u4fbf\u5feb\u901f\u5b9e\u73b0\u76ee\u7684: \r\n1. \u811a\u672c\u6ca1\u6709\u7ee7\u627f\u5355\u4f8b? \"\u8d85\u7ea7\u5355\u4f8b\"\u4e0d\u9700\u8981\u58f0\u540d, \u4e5f\u4e0d\u9700\u8981\u4fee\u6539\u4efb\u4f55\u539f\u6765\u7684\u811a\u672c\u5185\u5bb9, \u5f84\u76f4\u8c03\u7528\u5c31\u662f\u5355\u4f8b\u3002\r\n2. \u6570\u636e\u7ed3\u6784\u5b57\u6bb5\u5197\u4f59\u91cd\u590d? \"\u8f6f\u63a5\u53e3\" (SoftInterface) \u652f\u6301\u5feb\u901f\u6267\u884c\u53cd\u5c04\u83b7\u53d6\u548c\u8bbe\u7f6e\uff0c\u65b9\u6cd5\u59d4\u6258\uff0c\u5b57\u6bb5\u5c5e\u6027\u4e00\u884c\u8c03\u7528\u3002 \r\n3. untiy\u5bf9\u8c61\u8981\u9075\u5faa\u751f\u547d\u5468\u671f\u6c60\u5316\u592a\u9ebb\u70e6? \"\u8054\u5408\u5bf9\u8c61\u6c60\" (MacroPool.UnionSet) \u76f4\u63a5\u5c06\u8bbe\u5b9a\u9879\u76ee\u5f00\u653e\u5230inspector\u4e0a\u914d\u7f6e\uff0c\u4ee3\u7801\u4e2d\u53ea\u9700\u8981get\u548crelease\u5c31\u53ef\u4ee5! \r\n4. \u7a0b\u5e8fUI\u6846\u67b6\u548c\u903b\u8f91\u6846\u67b6\u9ad8\u5ea6\u8026\u5408\u65e0\u4ece\u4e0b\u624b? \u6709\u70b9\u9ebb\u70e6, \u4e0d\u8fc7\u83dc\u5355\u7279\u6027, \u67e5\u627e\u7279\u6027, \u547d\u540d\u6807\u8bb0\u7279\u6027\u53ef\u4ee5\u5e2e\u52a9\u4f60\u65e0\u89c6\u5185\u5bb9\u67e5\u627e\u5b57\u6bb5\u5c5e\u6027\u65b9\u6cd5\u7c7b\u578b\u7b49\u5185\u5bb9, \u4f7f\u7528 `XericUIActionVessel` \u63d2\u4ef6\u66f4\u662f\u5141\u8bb8\u76f4\u63a5\u5c06\u7c7b\u4f20\u5165\u5c31\u80fd\u7a0b\u5e8f\u5316\u751f\u6210\u83dc\u5355\u754c\u9762\u3002 \r\n5. \u9a71\u52a8ui\u7684\u6570\u636e\u91cf\u592a\u5927? \u63d0\u4f9b\u591a\u79cd\u57fa\u4e8eUI\u6846\u67b6\u7684\u6570\u636e\u7ed3\u6784\u865a\u62df\u5316\u6280\u672f, \u4f7f\u7528\u865a\u62df\u5316\u62c6\u5206\u6570\u636e\u53ef\u4ee5\u6bcf\u6b21\u5237\u65b0\u7684\u538b\u529b\u3002 \u4f7f\u7528\u6b64\u529f\u80fd\u9700\u8981\u5b89\u88c5XericUIActionVessel\u63d2\u4ef6, \u4ee3\u7801\u4ee5\u811a\u672c\u5448\u73b0\uff0c\u76f4\u63a5\u6d4f\u89c8\uff0c\u7f16\u5199\u65f6\u9075\u5faa\u7cbe\u7ec6\u7684\u5de5\u5382\u6743\u80fd\u5212\u5206\uff01 \r\n6. \u559c\u6b22\u51fd\u6570\u5f0f\u7f16\u7a0b\u548c\u4fbf\u6377\u7684\u8bed\u6cd5\u7cd6? \u63d2\u4ef6\u63d0\u4f9b\u591a\u79cd\u5de5\u5177\u7c7b: \u679a\u4e3e\u53ef\u4ee5\u4f7f\u7528`MacroEnum`\u5206\u7c7b\u548c`MacroEnum`\u6269\u5c55; \u6570\u5b66, \u5411\u91cf, \u51e0\u4f55, \u6570\u636e\u7ed3\u6784\u6269\u5c55, \u5e38\u89c1\u7b97\u6cd5, \u5e38\u7528\u5b57\u7b26\u8ba1\u7b97\u5df2\u7ecf\u5168\u90e8\u5185\u5d4c`MacroMath`\u5206\u7c7b; \r\n7. \u9700\u8981\u4f7f\u7528\u9ad8\u7ea7\u6570\u636e\u7ed3\u6784? \u5927\u9876\u5806, \u56db\u53c9\u6811, \u516b\u53c9\u6811, \u5b57\u5178\u6811, \u65f6\u95f4\u6233, \u591a\u6bb5\u7ebf, \u8d1d\u585e\u5c14, \u591a\u7ef4\u5e03\u5c14(\u6bd4\u7279\u77e2\u91cf), \u6bd4\u7279\u56fe, \u53cc\u751f\u54c8\u5e0c\u8868, \u76f8\u90bb\u7f51\u683c\u7b49\u5185\u5bb9\u53ef\u4ee5\u76f4\u63a5\u4f7f\u7528\u3002 \r\n8. \u60f3\u8981\u7acb\u523b\u80fd\u591f\u5728\u573a\u666f\u4e2d\u79fb\u52a8\u89d2\u8272? \u7b2c\u4e00\u4eba\u79f0, \u7b2c\u4e09\u4eba\u79f0, \u4e0a\u5e1d\u89c6\u89d2\u5df2\u7ecf\u51c6\u5907\u5c31\u7eea! \r\n9. \u538c\u70e6\u590d\u6742\u7684\u6309\u952e\u68c0\u67e5\u903b\u8f91? \u6309\u952e\u5b8f (MacroKey) \u63d0\u4f9b\u72b6\u6001\u8868\u5904\u7406\u66f4\u591a\u6309\u952e, \u53ef\u4ee5\u8bc6\u522b\u77ed\u6309, \u957f\u6309, \u53cc\u51fb, \u8fde\u51fb, \u62d6\u62fd, \u4ee5\u53ca\u5b83\u4eec\u7684\u6309\u4e0b\u548c\u91ca\u653e\u72b6\u6001! \u4e14\u53ef\u4ee5\u9009\u62e9\u4ee5\u65f6\u95f4, \u5750\u6807\u7b49\u5171\u8ba14\u79cd\u6a21\u5f0f\u89e6\u53d1, \u652f\u6301\u65b0\u8f93\u5165\u7cfb\u7edf, \u6ee1\u8db3\u7edd\u5927\u90e8\u5206\u573a\u666f\u7684\u7ec6\u5206\u9700\u6c42\u3002 \r\n10. \u9700\u8981\u9690\u85cf\u65e5\u5fd7\u8c03\u7528\u6808\u4fe1\u606f? XericLogger \u53ef\u4ee5\u51cf\u5c11\u65e5\u5fd7\u8f93\u51fa\u5185\u5bb9\u3002 \r\n11. \u9700\u8981\u66f4\u591a\u8c03\u8bd5\u4fe1\u606f? \u63d0\u4f9b\u53ef\u4ee5\u548c\u865a\u5e7b\u539f\u751f\u5ab2\u7f8e\u7684Gizmos\u8c03\u8bd5\u663e\u793a\u529f\u80fd\u3002 \r\n11. \u62c5\u5fc3\u517c\u5bb9\u6027\u5417? \u7a0b\u5e8f\u4e3b\u8981\u57fa\u4e8e2022\u7248\u672c\u5f00\u53d1, \u5411\u4e0b\u517c\u5bb92021, \u5411\u4e0a\u652f\u63016000, \u4e0d\u6d89\u53ca\u6e32\u67d3\u7ba1\u7ebfAPI, \u6709\u9488\u5bf9webgl\u7248\u672c\u7684\u5355\u72ec\u6784\u5efa\u7248\u672c, \u4e14\u7ecf\u8fc7\u9879\u76ee\u5b9e\u6218\u9a8c\u8bc1\u3002 \r\n\r\n\u66f4\u591a\u7f16\u8f91\u5668\u529f\u80fd\u53c2\u8003: \r\n * \u5b89\u88c5Xeric Blueprint Graph\u4ee5\u83b7\u53d6Unity\u4e0a\u84dd\u56fe\u8282\u70b9\u529f\u80fd, \u5e76\u652f\u6301Xeric\u51fd\u6570\u5e93\u529f\u80fd\u3002\r\n * \u5b89\u88c5Xeric UI Graph\u4ee5\u83b7\u53d6\u84dd\u56fe\u5316UI\u751f\u6210\u529f\u80fd, \u4f7f\u7528\u84dd\u56fe\u903b\u8f91\u63a7\u5236UI\u4ea4\u4e92, \u751f\u6210\u903b\u8f91, \u5e76\u652f\u6301Xeric\u51fd\u6570\u5e93\u529f\u80fd\u3002\r\n * \u5b89\u88c5Digital Twin Tool\u4ee5\u83b7\u53d6\u5e38\u7528\u8c03\u8bd5\u5de5\u5177\u96c6\u3002\r\n * \u5b89\u88c5Nexus Frame Flow\u4ee5\u83b7\u53d6\u5de5\u4f5c\u67b6\u6784\u6d41\u5904\u7406\u529f\u80fd\u3002\r\n * \u5b89\u88c5Xeric Editor\u8f85\u52a9\u5f15\u64ce\u5927\u7eb2\u7f8e\u5316\u7ec4\u4ef6\u3001 \u8d44\u6e90\u5783\u573e\u5904\u7406\u7ec4\u4ef6\u3001 \u8d44\u6e90\u7f13\u5b58\u7ec4\u4ef6\u3001 \u5feb\u901f\u622a\u56fe\u529f\u80fd, \u4ee5\u53ca\u66f4\u591a\u5feb\u6377\u952e\u529f\u80fd", "keywords": [