From 2e68a1eda53d5a8c842c06f385e1a5e36c0a7012 Mon Sep 17 00:00:00 2001 From: Minmoose Date: Thu, 27 Nov 2025 17:59:06 -0600 Subject: [PATCH 1/2] Add ImSequencer to Brio to fix build --- Brio/Brio.csproj | 5 + Brio/UI/Controls/Sequencer/Extensions.cs | 13 + .../Sequencer/ImCurveEdit/CurveContext.cs | 79 + .../Sequencer/ImCurveEdit/CurveType.cs | 11 + .../Sequencer/ImCurveEdit/EditPoint.cs | 40 + .../Sequencer/ImCurveEdit/ImCurveEdit.cs | 441 ++++++ Brio/UI/Controls/Sequencer/ImViewGuizmo.cs | 538 +++++++ .../Sequencer/Memory/AllocationCallbacks.cs | 168 ++ .../Controls/Sequencer/Memory/ArrayUtils.cs | 76 + .../Sequencer/Memory/IAllocationCallbacks.cs | 103 ++ .../Controls/Sequencer/Memory/IConverter1.cs | 7 + .../UI/Controls/Sequencer/Memory/IFreeable.cs | 13 + Brio/UI/Controls/Sequencer/Memory/LICENSE | 21 + Brio/UI/Controls/Sequencer/Memory/README | 1 + .../Controls/Sequencer/Memory/UnsafeList.cs | 659 ++++++++ Brio/UI/Controls/Sequencer/Memory/Utils.cs | 1379 +++++++++++++++++ .../Controls/Sequencer/SequenceInterface.cs | 27 + Brio/UI/Controls/Sequencer/Sequencer.cs | 559 +++++++ .../UI/Controls/Sequencer/SequencerContext.cs | 36 + .../Controls/Sequencer/SequencerCustomDraw.cs | 31 + .../UI/Controls/Sequencer/SequencerOptions.cs | 12 + Brio/UI/Controls/Sequencer/SequencerStyle.cs | 19 + Brio/UI/Controls/Sequencer/ZoomScrollbar.cs | 219 +++ 23 files changed, 4457 insertions(+) create mode 100644 Brio/UI/Controls/Sequencer/Extensions.cs create mode 100644 Brio/UI/Controls/Sequencer/ImCurveEdit/CurveContext.cs create mode 100644 Brio/UI/Controls/Sequencer/ImCurveEdit/CurveType.cs create mode 100644 Brio/UI/Controls/Sequencer/ImCurveEdit/EditPoint.cs create mode 100644 Brio/UI/Controls/Sequencer/ImCurveEdit/ImCurveEdit.cs create mode 100644 Brio/UI/Controls/Sequencer/ImViewGuizmo.cs create mode 100644 Brio/UI/Controls/Sequencer/Memory/AllocationCallbacks.cs create mode 100644 Brio/UI/Controls/Sequencer/Memory/ArrayUtils.cs create mode 100644 Brio/UI/Controls/Sequencer/Memory/IAllocationCallbacks.cs create mode 100644 Brio/UI/Controls/Sequencer/Memory/IConverter1.cs create mode 100644 Brio/UI/Controls/Sequencer/Memory/IFreeable.cs create mode 100644 Brio/UI/Controls/Sequencer/Memory/LICENSE create mode 100644 Brio/UI/Controls/Sequencer/Memory/README create mode 100644 Brio/UI/Controls/Sequencer/Memory/UnsafeList.cs create mode 100644 Brio/UI/Controls/Sequencer/Memory/Utils.cs create mode 100644 Brio/UI/Controls/Sequencer/SequenceInterface.cs create mode 100644 Brio/UI/Controls/Sequencer/Sequencer.cs create mode 100644 Brio/UI/Controls/Sequencer/SequencerContext.cs create mode 100644 Brio/UI/Controls/Sequencer/SequencerCustomDraw.cs create mode 100644 Brio/UI/Controls/Sequencer/SequencerOptions.cs create mode 100644 Brio/UI/Controls/Sequencer/SequencerStyle.cs create mode 100644 Brio/UI/Controls/Sequencer/ZoomScrollbar.cs diff --git a/Brio/Brio.csproj b/Brio/Brio.csproj index 0d69d999..34d8df6a 100644 --- a/Brio/Brio.csproj +++ b/Brio/Brio.csproj @@ -20,6 +20,11 @@ + + + + + diff --git a/Brio/UI/Controls/Sequencer/Extensions.cs b/Brio/UI/Controls/Sequencer/Extensions.cs new file mode 100644 index 00000000..de5a3481 --- /dev/null +++ b/Brio/UI/Controls/Sequencer/Extensions.cs @@ -0,0 +1,13 @@ +using System.Numerics; + +namespace ImSequencer; + +public class Extensions +{ + public static float ImLerp(float a, float b, float t) + { + return a + (b - a) * t; + } + public static Vector2 Add(Vector2 a, Vector2 b) => new Vector2(a.X + b.X, a.Y + b.Y); + +} diff --git a/Brio/UI/Controls/Sequencer/ImCurveEdit/CurveContext.cs b/Brio/UI/Controls/Sequencer/ImCurveEdit/CurveContext.cs new file mode 100644 index 00000000..db6c022f --- /dev/null +++ b/Brio/UI/Controls/Sequencer/ImCurveEdit/CurveContext.cs @@ -0,0 +1,79 @@ +using System.Numerics; +using Dalamud.Bindings.ImGui; +using ImSequencer.Memory; + +namespace ImSequencer.ImCurveEdit +{ + + public abstract unsafe class CurveContext + { + public bool focused = false; + public ImGuiIOPtr Io; + public ImDrawList* DrawList; + public Vector2 ScreenMin; + public Vector2 ScreenMax; + public Vector2 ScreenRange; + public Vector2 Min; + public Vector2 Max; + public Vector2 Range; + public Vector2 CanvasOrigin = new(0.5f); + + public Vector2 PointToCanvas(Vector2 pt) + { + return ((pt - Min) / Range) - CanvasOrigin; + } + + public Vector2 CanvasToPoint(Vector2 pt) + { + return (pt) * Range + Min; + } + + public Vector2 PointToScreen(Vector2 pt) + { + return pt * ScreenRange + ScreenMin; + } + + public Vector2 ScreenToPoint(Vector2 pt) + { + return (pt - ScreenMin) / ScreenRange; + } + + public Vector2 ScreenToCanvas(Vector2 pt) + { + return PointToCanvas(ScreenToPoint(pt)); + } + + public Vector2 CanvasToScreen(Vector2 pt) + { + return PointToScreen(CanvasToPoint(pt)); + } + + public abstract int GetCurveCount(); + + public virtual bool IsVisible(int curveIndex) + { return true; } + + public virtual CurveType GetCurveType(int curveIndex) + { return CurveType.CurveSmooth; } + + public abstract int GetPointCount(int curveIndex); + + public abstract uint GetCurveColor(int curveIndex); + + public abstract Vector2[] GetPoints(int curveIndex); + + public abstract int EditPoint(int curveIndex, int pointIndex, Vector2 value); + + public abstract void AddPoint(int curveIndex, Vector2 value); + + public virtual uint GetBackgroundColor() + { return 0x1B202020; } + + // handle undo/redo thru this functions + public virtual void BeginEdit(int index) + { } + + public virtual void EndEdit() + { } + }; +} \ No newline at end of file diff --git a/Brio/UI/Controls/Sequencer/ImCurveEdit/CurveType.cs b/Brio/UI/Controls/Sequencer/ImCurveEdit/CurveType.cs new file mode 100644 index 00000000..e5c962f9 --- /dev/null +++ b/Brio/UI/Controls/Sequencer/ImCurveEdit/CurveType.cs @@ -0,0 +1,11 @@ +namespace ImSequencer.ImCurveEdit +{ + public enum CurveType + { + None, + CurveDiscrete, + CurveLinear, + CurveSmooth, + CurveBezier, + }; +} \ No newline at end of file diff --git a/Brio/UI/Controls/Sequencer/ImCurveEdit/EditPoint.cs b/Brio/UI/Controls/Sequencer/ImCurveEdit/EditPoint.cs new file mode 100644 index 00000000..d07b9727 --- /dev/null +++ b/Brio/UI/Controls/Sequencer/ImCurveEdit/EditPoint.cs @@ -0,0 +1,40 @@ +namespace ImSequencer.ImCurveEdit +{ + public struct EditPoint + { + public int curveIndex; + public int pointIndex; + + public EditPoint(int curveIndex, int pointIndex) + { + this.curveIndex = curveIndex; + this.pointIndex = pointIndex; + } + + public static bool operator <(in EditPoint a, in EditPoint b) + { + if (a.curveIndex < b.curveIndex) + + return true; + if (a.curveIndex > b.curveIndex) + return false; + + if (a.pointIndex < b.pointIndex) + return true; + return false; + } + + public static bool operator >(in EditPoint a, in EditPoint b) + { + if (a.curveIndex > b.curveIndex) + + return true; + if (a.curveIndex < b.curveIndex) + return false; + + if (a.pointIndex > b.pointIndex) + return true; + return false; + } + }; +} \ No newline at end of file diff --git a/Brio/UI/Controls/Sequencer/ImCurveEdit/ImCurveEdit.cs b/Brio/UI/Controls/Sequencer/ImCurveEdit/ImCurveEdit.cs new file mode 100644 index 00000000..9186d1d7 --- /dev/null +++ b/Brio/UI/Controls/Sequencer/ImCurveEdit/ImCurveEdit.cs @@ -0,0 +1,441 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Dalamud.Bindings.ImGui; + +namespace ImSequencer.ImCurveEdit +{ + public static unsafe class ImCurveEdit + { + private static float Smoothstep(float edge0, float edge1, float x) + { + x = Math.Clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); + return x * x * (3 - 2 * x); + } + + private static float Distance(float x, float y, float x1, float y1, float x2, float y2) + { + float A = x - x1; + float B = y - y1; + float C = x2 - x1; + float D = y2 - y1; + + float dot = A * C + B * D; + float len_sq = C * C + D * D; + float param = -1.0f; + if (len_sq > float.Epsilon) + param = dot / len_sq; + + float xx, yy; + + if (param < 0.0f) + { + xx = x1; + yy = y1; + } + else if (param > 1.0f) + { + xx = x2; + yy = y2; + } + else + { + xx = x1 + param * C; + yy = y1 + param * D; + } + + float dx = x - xx; + float dy = y - yy; + return MathF.Sqrt(dx * dx + dy * dy); + } + + private static Vector2[] localOffsets = new Vector2[] { new Vector2(1, 0), new Vector2(0, 1), new Vector2(-1, 0), new Vector2(0, -1) }; + + private static int DrawPoint(CurveContext ctx, Vector2 pos, bool edited) + { + int ret = 0; + var draw = ctx.DrawList; + var io = ctx.Io; + + Vector2 center = ctx.CanvasToScreen(pos); + Vector2* offsets = stackalloc Vector2[4]; + for (int i = 0; i < 4; i++) + { + offsets[i] = center + localOffsets[i] * 4.5f; + } + + ImRect anchor = new(center - new Vector2(5, 5), center + new Vector2(5, 5)); + draw->AddConvexPolyFilled(offsets, 4, 0xFFAA00AA); + if (anchor.Contains(io.MousePos)) + { + ret = 1; + if (io.MouseDown[0]) + ret = 2; + } + if (edited) + draw->AddPolyline(offsets, 4, 0xFFFFFFFF, ImDrawFlags.Closed, 3.0f); + else if (ret != 0) + draw->AddPolyline(offsets, 4, 0xFF80B0FF, ImDrawFlags.Closed, 2.0f); + else + draw->AddPolyline(offsets, 4, 0xFF0080FF, ImDrawFlags.Closed, 2.0f); + + return ret; + } + + static bool selectingQuad = false; + static Vector2 quadSelection; + static int overCurve = -1; + static int movingCurve = -1; + static bool scrollingV = false; + static HashSet selection = new(); + static Queue<(EditPoint, bool removeOrAdd)> selectionOpQueue = new(); + static bool overSelectedPoint = false; + + static bool pointsMoved = false; + static Vector2 mousePosOrigin; + static ImVector originalPoints; + + public static int Edit(CurveContext ctx, Vector2 size, uint id, ImRect* clippingRect = null, ImVector* selectedPoints = null) + { + int ret = 0; + var io = ImGui.GetIO(); + var window = ImGuiP.GetCurrentWindow(); + var pos = ImGui.GetCursorScreenPos(); + ImRect bb = new(pos, pos + size); + ImGuiP.ItemSize(bb); + + if (!ImGuiP.ItemAdd(bb, id, ref bb, ImGuiItemFlags.None)) + { + return ret; + } + + bool hovered = ImGuiP.ItemHoverable(bb, id); + ctx.Io = io; + ctx.ScreenMin = bb.Min; + ctx.ScreenMax = bb.Max; + ctx.ScreenRange = size; + ctx.Range = ctx.Max - ctx.Min; + ctx.focused = ImGui.IsItemFocused(); + ImDrawList* draw = ImGui.GetWindowDrawList(); + + if(clippingRect != null) + draw->PushClipRect(clippingRect->Min, clippingRect->Max, false); + ctx.DrawList = draw; + + int curveCount = ctx.GetCurveCount(); + + draw->AddRectFilled(pos, pos + size, ctx.GetBackgroundColor()); + draw->AddLine(ctx.PointToScreen(new(0.0f, 0.5f)), ctx.PointToScreen(new(1.0f, 0.5f)), 0xFF000000, 1.5f); + + bool overCurveOrPoint = false; + int localOverCurve = -1; + int highLightedCurveIndex = -1; + + for (int c = 0; c < curveCount; ++c) + { + if (c == overCurve || !ctx.IsVisible(c)) + continue; + int ptCount = ctx.GetPointCount(c); + if (ptCount < 1) + continue; + CurveType curveType = ctx.GetCurveType(c); + if (curveType == CurveType.None) + continue; + + DrawCurve(ctx, ref overCurveOrPoint, ref localOverCurve, highLightedCurveIndex, c, ptCount, curveType); + } + + if (overCurve != -1) + { + int c = overCurve; + int ptCount = ctx.GetPointCount(c); + CurveType curveType = ctx.GetCurveType(c); + if (ptCount > 0 && curveType != CurveType.None) + { + DrawCurve(ctx, ref overCurveOrPoint, ref localOverCurve, highLightedCurveIndex, c, ptCount, curveType); + } + } + + if (localOverCurve == -1) + overCurve = -1; + + // move selection + + if (overSelectedPoint && ImGui.IsMouseDown(ImGuiMouseButton.Left)) + { + if (io.MouseDelta != Vector2.Zero && selection.Count != 0) + { + if (!pointsMoved) + { + ctx.BeginEdit(0); + mousePosOrigin = io.MousePos; + originalPoints.Resize(selection.Count); + int index = 0; + foreach (var sel in selection) + { + var pts = ctx.GetPoints(sel.curveIndex); + originalPoints[index++] = pts[sel.pointIndex]; + } + } + pointsMoved = true; + ret = 1; + + Vector2 delta = ctx.ScreenToCanvas(io.MousePos) - ctx.ScreenToCanvas(mousePosOrigin); + int originalIndex = 0; + foreach (var sel in selection) + { + Vector2 p = originalPoints[originalIndex] + delta; + int newIndex = ctx.EditPoint(sel.curveIndex, sel.pointIndex, p); + if (newIndex != sel.pointIndex) + { + EditPoint newSel = new(sel.curveIndex, newIndex); + selectionOpQueue.Enqueue((sel, false)); + selectionOpQueue.Enqueue((newSel, true)); + } + originalIndex++; + } + + while (selectionOpQueue.Count > 0) + { + var result = selectionOpQueue.Dequeue(); + if (result.removeOrAdd) + { + selection.Add(result.Item1); + } + else + { + selection.Remove(result.Item1); + } + } + } + } + + if (overSelectedPoint && !io.MouseDown[0]) + { + overSelectedPoint = false; + if (pointsMoved) + { + pointsMoved = false; + ctx.EndEdit(); + } + } + + // add point + if (overCurve != -1 && io.MouseDoubleClicked[0]) + { + Vector2 np = ctx.ScreenToCanvas(io.MousePos); + ctx.BeginEdit(overCurve); + ctx.AddPoint(overCurve, np); + ctx.EndEdit(); + ret = 1; + } + + // move curve + // TODO: add undo redo + if (movingCurve != -1) + { + int ptCount = ctx.GetPointCount(movingCurve); + var pts = ctx.GetPoints(movingCurve); + + if (!pointsMoved) + { + ImGui.SetNextFrameWantCaptureMouse(true); + ImGuiP.SetActiveID(id, window); + mousePosOrigin = io.MousePos; + pointsMoved = true; + originalPoints.Resize(ptCount); + for (int index = 0; index < ptCount; index++) + { + originalPoints[index] = pts[index]; + } + } + if (ptCount >= 1) + { + Vector2 delta = ctx.ScreenToCanvas(io.MousePos) - ctx.ScreenToCanvas(mousePosOrigin); + for (int p = 0; p < ptCount; p++) + { + ctx.EditPoint(movingCurve, p, originalPoints[p] + delta); + } + ret = 1; + } + if (!io.MouseDown[0]) + { + ImGuiP.ClearActiveID(); + movingCurve = -1; + pointsMoved = false; + ctx.EndEdit(); + } + } + if (movingCurve == -1 && overCurve != -1 && ImGui.IsMouseClicked(0) && selection.Count != 0 && !selectingQuad) + { + movingCurve = overCurve; + ctx.BeginEdit(overCurve); + } + + // quad selection + if (selectingQuad) + { + Vector2 bmin = Vector2.Min(quadSelection, io.MousePos); + Vector2 bmax = Vector2.Max(quadSelection, io.MousePos); + draw->AddRectFilled(bmin, bmax, 0x40FF0000, 1.0f); + draw->AddRect(bmin, bmax, 0xFFFF0000, 1.0f); + ImRect selectionQuad = new(bmin, bmax); + if (!io.MouseDown[0]) + { + if (!io.KeyShift) + selection.Clear(); + // select everythnig is quad + for (int c = 0; c < curveCount; c++) + { + if (!ctx.IsVisible(c)) + continue; + + int ptCount = ctx.GetPointCount(c); + if (ptCount < 1) + continue; + + var pts = ctx.GetPoints(c); + for (int p = 0; p < ptCount; p++) + { + Vector2 center = ctx.CanvasToScreen(pts[p]); + if (selectionQuad.Contains(center)) + selection.Add(new EditPoint(c, p)); + } + } + // done + selectingQuad = false; + } + } + if (!overCurveOrPoint && ImGui.IsMouseClicked(0) && !selectingQuad && movingCurve == -1 && !overSelectedPoint && hovered) + { + selectingQuad = true; + quadSelection = io.MousePos; + } + + if (selectedPoints != null) + { + selectedPoints->Resize(selection.Count); + int index = 0; + foreach (var point in selection) + (*selectedPoints)[index++] = point; + } + + if (hovered) + { + if (MathF.Abs(io.MouseWheel) > float.Epsilon) + { + float r = (io.MousePos.Y - pos.Y) / size.Y; + float ratioY = Extensions.ImLerp(ctx.Min.Y, ctx.Max.Y, r); + static float ScaleValue(float wheel, float v, float ratio) + { + v -= ratio; + v *= 1.0f - wheel * 0.05f; + v += ratio; + return v; + } + + var wheel = io.MouseWheel; + ctx.Min.Y = ScaleValue(wheel, ctx.Min.Y, ratioY); + ctx.Max.Y = ScaleValue(wheel, ctx.Max.Y, ratioY); + } + if (!scrollingV && ImGui.IsMouseDown(ImGuiMouseButton.Middle)) + { + scrollingV = true; + } + } + + if (scrollingV) + { + float deltaH = io.MouseDelta.Y * ctx.Range.Y; + ctx.Min.Y -= deltaH; + ctx.Max.Y -= deltaH; + if (!ImGui.IsMouseDown(ImGuiMouseButton.Middle)) + scrollingV = false; + } + if(clippingRect != null) + draw->PopClipRect(); + return ret; + } + + private static void DrawCurve(CurveContext ctx, ref bool overCurveOrPoint, ref int localOverCurve, int highLightedCurveIndex, int c, int ptCount, CurveType curveType) + { + ctx.Io = ImGui.GetIO(); + ctx.DrawList = ImGui.GetWindowDrawList(); + var io = ctx.Io; + var draw = ctx.DrawList; + var pts = ctx.GetPoints(c); + uint curveColor = ctx.GetCurveColor(c); + if (c == highLightedCurveIndex && selection.Count == 0 && !selectingQuad || movingCurve == c) + curveColor = 0xFFFFFFFF; + + for (int p = 0; p < ptCount - 1; p++) + { + Vector2 p1 = ctx.CanvasToPoint(pts[p]); + Vector2 p2 = ctx.CanvasToPoint(pts[p + 1]); + + if (curveType == CurveType.CurveSmooth || curveType == CurveType.CurveLinear) + { + nuint subStepCount = (curveType == CurveType.CurveSmooth) ? 20u : 2u; + float step = 1.0f / (subStepCount - 1); + for (nuint substep = 0; substep < subStepCount - 1; substep++) + { + float t = substep * step; + + Vector2 sp1 = Vector2.Lerp(p1, p2, t); + Vector2 sp2 = Vector2.Lerp(p1, p2, t + step); + + float rt1 = Smoothstep(p1.X, p2.X, sp1.X); + float rt2 = Smoothstep(p1.X, p2.X, sp2.X); + + Vector2 pos1 = ctx.PointToScreen(new(sp1.X, Extensions.ImLerp(p1.Y, p2.Y, rt1))); + Vector2 pos2 = ctx.PointToScreen(new(sp2.X, Extensions.ImLerp(p1.Y, p2.Y, rt2))); + + if (Distance(io.MousePos.X, io.MousePos.Y, pos1.X, pos1.Y, pos2.X, pos2.Y) < 8.0f && !scrollingV) + { + localOverCurve = c; + overCurve = c; + overCurveOrPoint = true; + } + + draw->AddLine(pos1, pos2, curveColor, 1.3f); + } // substep + } + else if (curveType == CurveType.CurveDiscrete) + { + Vector2 dp1 = ctx.PointToScreen(p1); + Vector2 dp2 = ctx.PointToScreen(new(p2.X, p1.Y)); + Vector2 dp3 = ctx.PointToScreen(p2); + draw->AddLine(dp1, dp2, curveColor, 1.3f); + draw->AddLine(dp2, dp3, curveColor, 1.3f); + + if (Distance(io.MousePos.X, io.MousePos.Y, dp1.X, dp1.Y, dp3.X, dp1.Y) < 8.0f || + Distance(io.MousePos.X, io.MousePos.Y, dp3.X, dp1.Y, dp3.X, dp3.Y) < 8.0f + /*&& localOverCurve == -1*/) + { + localOverCurve = c; + overCurve = c; + overCurveOrPoint = true; + } + } + } + + for (int p = 0; p < ptCount; p++) + { + int drawState = DrawPoint(ctx, pts[p], selection.Contains(new EditPoint(c, p)) && movingCurve == -1 && !scrollingV); + if (drawState != 0 && movingCurve == -1 && !selectingQuad) + { + overCurveOrPoint = true; + overSelectedPoint = true; + overCurve = -1; + if (drawState == 2) + { + var point = new EditPoint(c, p); + if (!io.KeyShift && !selection.Contains(point)) + selection.Clear(); + selection.Add(point); + } + } + } + } + } +} diff --git a/Brio/UI/Controls/Sequencer/ImViewGuizmo.cs b/Brio/UI/Controls/Sequencer/ImViewGuizmo.cs new file mode 100644 index 00000000..cd33959a --- /dev/null +++ b/Brio/UI/Controls/Sequencer/ImViewGuizmo.cs @@ -0,0 +1,538 @@ +// ImViewGuizmo.cs - C# port from ImViewGuizmo.h +// Uses System.Numerics and Hexa.NET.ImGui +// Ported from https://github.com/Ka1serM/ImViewGuizmo/blob/104a4e1cf20e87fff1daa264690e0f9f446920e1/ImViewGuizmo.h + +using System; +using System.Numerics; +using System.Collections.Generic; +using Dalamud.Bindings.ImGui; + +namespace ImViewGuizmo +{ + public static class ImViewGuizmo + { + // --- INTERFACE --- + + public class Style + { + public float Scale = 1f; + + // Axis visuals + public float LineLength = 0.5f; + public float LineWidth = 4.0f; + public float CircleRadius = 15.0f; + public float FadeFactor = 0.25f; + + // Highlight + public uint HighlightColor = ImGui.ColorConvertFloat4ToU32(new Vector4(1f, 1f, 0f, 1f)); + public float HighlightWidth = 2.0f; + + // Axis + public uint[] AxisColors = new uint[3] + { + ImGui.ColorConvertFloat4ToU32(new Vector4(230/255f, 51/255f, 51/255f, 1f)), // X + ImGui.ColorConvertFloat4ToU32(new Vector4(51/255f, 230/255f, 51/255f, 1f)), // Y + ImGui.ColorConvertFloat4ToU32(new Vector4(51/255f, 128/255f, 1f, 1f)) // Z + }; + + // Labels + public float LabelSize = 1.0f; + public string[] AxisLabels = new string[3] { "X", "Y", "Z" }; + public uint LabelColor = ImGui.ColorConvertFloat4ToU32(new Vector4(1f, 1f, 1f, 1f)); + + // Big Circle + public float BigCircleRadius = 80.0f; + public uint BigCircleColor = ImGui.ColorConvertFloat4ToU32(new Vector4(1f, 1f, 1f, 0.196f)); + + // Animation + public bool AnimateSnap = true; + public float SnapAnimationDuration = 0.3f; // seconds + + // Zoom/Pan Button Visuals + public float ToolButtonRadius = 25f; + public float ToolButtonInnerPadding = 4f; + public uint ToolButtonColor = ImGui.ColorConvertFloat4ToU32(new Vector4(0.565f, 0.565f, 0.565f, 0.196f)); + public uint ToolButtonHoveredColor = ImGui.ColorConvertFloat4ToU32(new Vector4(0.843f, 0.843f, 0.843f, 0.196f)); + public uint ToolButtonIconColor = ImGui.ColorConvertFloat4ToU32(new Vector4(0.843f, 0.843f, 0.843f, 0.882f)); + } + + private static Style _style = new Style(); + public static ref Style GetStyle() => ref _style; + + public struct GizmoAxis + { + public int Id; + public int AxisIndex; + public float Depth; + public Vector3 Direction; + } + + public enum ActiveTool + { + None, + Gizmo, + Zoom, + Pan + } + + public const float BaseSize = 256f; + public static readonly Vector3 Origin = Vector3.Zero; + public static readonly Vector3 WorldRight = new(1f, 0f, 0f); + public static readonly Vector3 WorldUp = new(0f, -1f, 0f); + public static readonly Vector3 WorldForward = new(0f, 0f, 1f); + public static readonly Vector3[] AxisVectors = new[] + { + new Vector3(1,0,0), + new Vector3(0,1,0), + new Vector3(0,0,1) + }; + + public class Context + { + public int HoveredAxisId = -1; + public bool IsZoomButtonHovered = false; + public bool IsPanButtonHovered = false; + public ActiveTool ActiveTool = ActiveTool.None; + + // Animation state + public bool IsAnimating = false; + public float AnimationStartTime = 0f; + public Vector3 StartPos; + public Vector3 TargetPos; + public Vector3 StartUp; + public Vector3 TargetUp; + + public bool IsHoveringGizmo() => HoveredAxisId != -1; + public void Reset() + { + HoveredAxisId = -1; + IsZoomButtonHovered = false; + IsPanButtonHovered = false; + ActiveTool = ActiveTool.None; + } + } + + private static Context _context = new(); + public static ref Context GetContext() => ref _context; + + public static bool IsUsing() => GetContext().ActiveTool != ActiveTool.None; + + public static bool IsOver() + { + var ctx = GetContext(); + return ctx.HoveredAxisId != -1 || ctx.IsZoomButtonHovered || ctx.IsPanButtonHovered; + } + + // --- Math Helpers --- + + public static float Mix(float a, float b, float t) => a * (1f - t) + b * t; + public static Vector3 Mix(Vector3 a, Vector3 b, float t) => a * (1f - t) + b * t; + + + // replace with https://stackoverflow.com/questions/12435671/quaternion-lookat-function/51170230#51170230 + public static Quaternion QuaternionLookAt(Vector3 forward, Vector3 up) + { + var z = Vector3.Normalize(forward); + var x = Vector3.Normalize(Vector3.Cross(up, z)); + var y = Vector3.Cross(z, x); + + float m00 = x.X, m01 = y.X, m02 = z.X; + float m10 = x.Y, m11 = y.Y, m12 = z.Y; + float m20 = x.Z, m21 = y.Z, m22 = z.Z; + + float num8 = (m00 + m11) + m22; + Quaternion q = new(); + if (num8 > 0f) + { + float num = MathF.Sqrt(num8 + 1f); + q.W = num * 0.5f; + num = 0.5f / num; + q.X = (m12 - m21) * num; + q.Y = (m20 - m02) * num; + q.Z = (m01 - m10) * num; + return q; + } + if ((m00 >= m11) && (m00 >= m22)) + { + float num7 = MathF.Sqrt(((1f + m00) - m11) - m22); + float num4 = 0.5f / num7; + q.X = 0.5f * num7; + q.Y = (m01 + m10) * num4; + q.Z = (m02 + m20) * num4; + q.W = (m12 - m21) * num4; + return q; + } + if (m11 > m22) + { + float num6 = MathF.Sqrt(((1f + m11) - m00) - m22); + float num3 = 0.5f / num6; + q.X = (m10 + m01) * num3; + q.Y = 0.5f * num6; + q.Z = (m21 + m12) * num3; + q.W = (m20 - m02) * num3; + return q; + } + float num5 = MathF.Sqrt(((1f + m22) - m00) - m11); + float num2 = 0.5f / num5; + q.X = (m20 + m02) * num2; + q.Y = (m21 + m12) * num2; + q.Z = 0.5f * num5; + q.W = (m01 - m10) * num2; + return q; + } + + // --- Main API --- + + private static int _lastFrame = -1; + private static void BeginFrame() + { + int currentFrame = ImGui.GetFrameCount(); + if (_lastFrame != currentFrame) + { + _lastFrame = currentFrame; + var ctx = GetContext(); + ctx.HoveredAxisId = -1; + ctx.IsZoomButtonHovered = false; + ctx.IsPanButtonHovered = false; + } + } + + public static bool Rotate(ref Vector3 cameraPos, ref Quaternion cameraRot, Vector2 position, float snapDistance = 5f, float rotationSpeed = 0.005f) + { + BeginFrame(); + var io = ImGui.GetIO(); + var drawList = ImGui.GetWindowDrawList(); + var ctx = GetContext(); + var style = GetStyle(); + bool wasModified = false; + + if (!ImGui.IsMouseDown(0) && ctx.ActiveTool != ActiveTool.None) + ctx.ActiveTool = ActiveTool.None; + + // Animation logic + if (ctx.IsAnimating) + { + float elapsedTime = (float)ImGui.GetTime() - ctx.AnimationStartTime; + float t = MathF.Min(1.0f, elapsedTime / style.SnapAnimationDuration); + t = 1.0f - (1.0f - t) * (1.0f - t); + + Vector3 currentDir = ctx.StartPos.Length() > 0.0001f && ctx.TargetPos.Length() > 0.0001f + ? Vector3.Normalize(Mix(Vector3.Normalize(ctx.StartPos), Vector3.Normalize(ctx.TargetPos), t)) + : Vector3.Normalize(Mix(new Vector3(0, 0, 1), Vector3.Normalize(ctx.TargetPos), t)); + float startDistance = ctx.StartPos.Length(); + float targetDistance = ctx.TargetPos.Length(); + float currentDistance = Mix(startDistance, targetDistance, t); + cameraPos = currentDir * currentDistance; + + Vector3 currentUp = Vector3.Normalize(Mix(ctx.StartUp, ctx.TargetUp, t)); + cameraRot = QuaternionLookAt(Vector3.Normalize(cameraPos), currentUp); + + wasModified = true; + + if (t >= 1.0f) + { + cameraPos = ctx.TargetPos; + cameraRot = QuaternionLookAt(Vector3.Normalize(ctx.TargetPos), ctx.TargetUp); + ctx.IsAnimating = false; + } + } + + float gizmoDiameter = BaseSize * style.Scale; + float scaledCircleRadius = style.CircleRadius * style.Scale; + float scaledBigCircleRadius = style.BigCircleRadius * style.Scale; + float scaledLineWidth = style.LineWidth * style.Scale; + float scaledHighlightWidth = style.HighlightWidth * style.Scale; + float scaledHighlightRadius = (style.CircleRadius + 2.0f) * style.Scale; + float scaledFontSize = ImGui.GetFontSize() * style.Scale * style.LabelSize; + + // Matrices + Matrix4x4 worldMatrix = Matrix4x4.CreateFromQuaternion(cameraRot) * Matrix4x4.CreateTranslation(cameraPos); + Matrix4x4 viewMatrix = Matrix4x4.Invert(worldMatrix, out var inv) ? inv : Matrix4x4.Identity; + Matrix4x4 gizmoViewMatrix = new( + viewMatrix.M11, viewMatrix.M12, viewMatrix.M13, 0, + viewMatrix.M21, viewMatrix.M22, viewMatrix.M23, 0, + viewMatrix.M31, viewMatrix.M32, viewMatrix.M33, 0, + 0, 0, 0, 1 + ); + Matrix4x4 gizmoProjectionMatrix = Matrix4x4.CreateOrthographicOffCenter(-1f, 1f, -1f, 1f, -100f, 100f); + Matrix4x4 gizmoMvp = gizmoViewMatrix * gizmoProjectionMatrix; + + // Axes + List axes = new(); + for (int i = 0; i < 3; ++i) + { + axes.Add(new GizmoAxis { Id = i * 2, AxisIndex = i, Depth = Vector3.TransformNormal(AxisVectors[i], gizmoViewMatrix).Z, Direction = AxisVectors[i] }); + axes.Add(new GizmoAxis { Id = i * 2 + 1, AxisIndex = i, Depth = Vector3.TransformNormal(-AxisVectors[i], gizmoViewMatrix).Z, Direction = -AxisVectors[i] }); + } + axes.Sort((a, b) => a.Depth.CompareTo(b.Depth)); + + Vector2 WorldToScreen(Vector3 worldPos) + { + Vector4 clipPos = Vector4.Transform(new Vector4(worldPos, 1f), gizmoMvp); + if (clipPos.W == 0.0f) return new(float.NegativeInfinity, float.NegativeInfinity); + Vector3 ndc = new(clipPos.X, clipPos.Y, clipPos.Z); + ndc /= clipPos.W; + return new( + position.X + ndc.X * (gizmoDiameter / 2f), + position.Y - ndc.Y * (gizmoDiameter / 2f) + ); + } + + // Hover logic + if (ctx.ActiveTool == ActiveTool.None && !ctx.IsAnimating) + { + float halfGizmoSize = gizmoDiameter / 2f; + var mousePos = io.MousePos; + float distToCenterSq = (mousePos - position).LengthSquared(); + + if (distToCenterSq < (halfGizmoSize + scaledCircleRadius) * (halfGizmoSize + scaledCircleRadius)) + { + float minDistanceSq = scaledCircleRadius * scaledCircleRadius; + foreach (var axis in axes) + { + if (axis.Depth < -0.1f) + continue; + var handlePos = WorldToScreen(axis.Direction * style.LineLength); + if ((handlePos - mousePos).LengthSquared() < minDistanceSq) + ctx.HoveredAxisId = axis.Id; + } + if (ctx.HoveredAxisId == -1) + { + var centerPos = WorldToScreen(Origin); + if ((centerPos - mousePos).LengthSquared() < scaledBigCircleRadius * scaledBigCircleRadius) + ctx.HoveredAxisId = 6; + } + } + } + + // Big circle + if (ctx.HoveredAxisId == 6 || ctx.ActiveTool == ActiveTool.Gizmo) + drawList.AddCircleFilled(WorldToScreen(Origin), scaledBigCircleRadius, style.BigCircleColor); + + // Draw axes + foreach (var axis in axes) + { + float factor = Mix(style.FadeFactor, 1.0f, (axis.Depth + 1.0f) * 0.5f); + var baseColor = ImGui.ColorConvertU32ToFloat4(style.AxisColors[axis.AxisIndex]); + var fadedColor = new Vector4(baseColor.X, baseColor.Y, baseColor.Z, baseColor.W * factor); + uint finalColor = ImGui.ColorConvertFloat4ToU32(fadedColor); + + var originPos = WorldToScreen(Origin); + var handlePos = WorldToScreen(axis.Direction * style.LineLength); + + var lineDir = handlePos - originPos; + float lineLen = lineDir.Length() + 1e-6f; + lineDir /= lineLen; + var lineEndPos = handlePos - lineDir * scaledCircleRadius; + + drawList.AddLine(originPos, lineEndPos, finalColor, scaledLineWidth); + drawList.AddCircleFilled(handlePos, scaledCircleRadius, finalColor); + + if (ctx.HoveredAxisId == axis.Id) + drawList.AddCircle(handlePos, scaledHighlightRadius, style.HighlightColor, 0, scaledHighlightWidth); + } + + // Draw labels + var font = ImGui.GetFont(); + foreach (var axis in axes) + { + if (axis.Depth < -0.1f) + continue; + var textPos = WorldToScreen(axis.Direction * style.LineLength); + string label = style.AxisLabels[axis.AxisIndex]; + var remaining = 0; + var textSize = ImGui.CalcTextSizeA(font, scaledFontSize, float.MaxValue, 0, label, out remaining ); //font.CalcTextSizeA(scaledFontSize, float.MaxValue, 0, label); + drawList.AddText(font, scaledFontSize, new(textPos.X - textSize.X * 0.5f, textPos.Y - textSize.Y * 0.5f), style.LabelColor, label); + } + + // Drag logic + if (ImGui.IsMouseDown(0)) + { + if (ctx.ActiveTool == ActiveTool.None && ctx.HoveredAxisId == 6) + { + ctx.ActiveTool = ActiveTool.Gizmo; + ctx.IsAnimating = false; + } + } + if (ctx.ActiveTool == ActiveTool.Gizmo) + { + float yawAngle = -io.MouseDelta.X * rotationSpeed; + float pitchAngle = -io.MouseDelta.Y * rotationSpeed; + var yawRotation = Quaternion.CreateFromAxisAngle(WorldUp, yawAngle); + var rightAxis = Vector3.Transform(WorldRight, cameraRot); + var pitchRotation = Quaternion.CreateFromAxisAngle(rightAxis, pitchAngle); + var totalRotation = Quaternion.Normalize(Quaternion.Concatenate(yawRotation, pitchRotation)); + cameraPos = Vector3.Transform(cameraPos, totalRotation); + cameraRot = Quaternion.Normalize(Quaternion.Concatenate(totalRotation, cameraRot)); + wasModified = true; + } + + // Snap + if (ImGui.IsMouseReleased(0) && ctx.HoveredAxisId >= 0 && ctx.HoveredAxisId <= 5 && !ImGui.IsMouseDragging(0)) + { + int axisIndex = ctx.HoveredAxisId / 2; + float sign = (ctx.HoveredAxisId % 2 == 0) ? -1.0f : 1.0f; + Vector3 targetDir = sign * AxisVectors[axisIndex]; + Vector3 targetPosition = targetDir * snapDistance; + + Vector3 up = WorldUp; + if (axisIndex == 1) up = WorldForward; + Vector3 targetUp = -up; + + var targetRotation = QuaternionLookAt(targetDir, targetUp); + + if (style.AnimateSnap && style.SnapAnimationDuration > 0.0f) + { + bool posIsDifferent = Vector3.DistanceSquared(cameraPos, targetPosition) > 0.0001f; + bool rotIsDifferent = (1.0f - MathF.Abs(Quaternion.Dot(cameraRot, targetRotation))) > 0.0001f; + + if (posIsDifferent || rotIsDifferent) + { + ctx.IsAnimating = true; + ctx.AnimationStartTime = (float)ImGui.GetTime(); + ctx.StartPos = cameraPos; + ctx.TargetPos = targetPosition; + ctx.StartUp = Vector3.Transform(new Vector3(0, 1, 0), cameraRot); + ctx.TargetUp = targetUp; + } + } + else + { + cameraRot = targetRotation; + cameraPos = targetPosition; + wasModified = true; + } + } + + return wasModified; + } + + public static bool Zoom(ref Vector3 cameraPos, Quaternion cameraRot, Vector2 position, float zoomSpeed = 0.005f) + { + BeginFrame(); + var io = ImGui.GetIO(); + var drawList = ImGui.GetWindowDrawList(); + var ctx = GetContext(); + var style = GetStyle(); + bool wasModified = false; + + float radius = style.ToolButtonRadius * style.Scale; + var center = new Vector2(position.X + radius, position.Y + radius); + + bool isHovered = false; + if (ctx.ActiveTool == ActiveTool.None || ctx.ActiveTool == ActiveTool.Zoom) + if ((io.MousePos - center).LengthSquared() < radius * radius) + isHovered = true; + + ctx.IsZoomButtonHovered = isHovered; + + if (isHovered && ImGui.IsMouseDown(0) && ctx.ActiveTool == ActiveTool.None) + { + ctx.ActiveTool = ActiveTool.Zoom; + ctx.IsAnimating = false; + } + + if (ctx.ActiveTool == ActiveTool.Zoom) + { + if (io.MouseDelta.Y != 0.0f) + { + Vector3 cameraForward = Vector3.Transform(WorldForward, cameraRot); + cameraPos += cameraForward * -io.MouseDelta.Y * zoomSpeed; + wasModified = true; + } + } + + uint bgColor = style.ToolButtonColor; + if (ctx.ActiveTool == ActiveTool.Zoom || isHovered) + bgColor = style.ToolButtonHoveredColor; + drawList.AddCircleFilled(center, radius, bgColor); + + float p = style.ToolButtonInnerPadding * style.Scale; + float th = 2.0f * style.Scale; + uint iconColor = style.ToolButtonIconColor; + + const float iconScale = 0.5f; + var glassCenter = new Vector2(center.X - (p / 2.0f) * iconScale, center.Y - (p / 2.0f) * iconScale); + float glassRadius = (radius - p - 1f) * iconScale; + drawList.AddCircle(glassCenter, glassRadius, iconColor, 0, th); + + var handleStart = new Vector2(center.X + (radius / 2.0f) * iconScale, center.Y + (radius / 2.0f) * iconScale); + var handleEnd = new Vector2(center.X + (radius - p) * iconScale, center.Y + (radius - p) * iconScale); + drawList.AddLine(handleStart, handleEnd, iconColor, th); + + var plusVertStart = new Vector2(center.X - (p / 2.0f) * iconScale, center.Y - (radius / 2.0f) * iconScale); + var plusVertEnd = new Vector2(center.X - (p / 2.0f) * iconScale, center.Y + (radius / 2.0f - p) * iconScale); + drawList.AddLine(plusVertStart, plusVertEnd, iconColor, th); + + var plusHorizStart = new Vector2(center.X + (-radius / 2.0f + p / 2.0f) * iconScale, center.Y - (p / 2.0f) * iconScale); + var plusHorizEnd = new Vector2(center.X + (radius / 2.0f - p * 1.5f) * iconScale, center.Y - (p / 2.0f) * iconScale); + drawList.AddLine(plusHorizStart, plusHorizEnd, iconColor, th); + + return wasModified; + } + + public static bool Pan(ref Vector3 cameraPos, Quaternion cameraRot, Vector2 position, float panSpeed = 0.001f) + { + BeginFrame(); + var io = ImGui.GetIO(); + var drawList = ImGui.GetWindowDrawList(); + var ctx = GetContext(); + var style = GetStyle(); + bool wasModified = false; + + float radius = style.ToolButtonRadius * style.Scale; + var center = new Vector2(position.X + radius, position.Y + radius); + + bool isHovered = false; + if (ctx.ActiveTool == ActiveTool.None || ctx.ActiveTool == ActiveTool.Pan) + if ((io.MousePos - center).LengthSquared() < radius * radius) + isHovered = true; + ctx.IsPanButtonHovered = isHovered; + + if (isHovered && ImGui.IsMouseDown(0) && ctx.ActiveTool == ActiveTool.None) + { + ctx.ActiveTool = ActiveTool.Pan; + ctx.IsAnimating = false; + } + + if (ctx.ActiveTool == ActiveTool.Pan) + { + if (io.MouseDelta.X != 0.0f || io.MouseDelta.Y != 0.0f) + { + cameraPos += Vector3.Transform(WorldRight, cameraRot) * -io.MouseDelta.X * panSpeed; + cameraPos += Vector3.Transform(WorldUp, cameraRot) * io.MouseDelta.Y * panSpeed; + wasModified = true; + } + } + + uint bgColor = style.ToolButtonColor; + if (ctx.ActiveTool == ActiveTool.Pan || isHovered) + bgColor = style.ToolButtonHoveredColor; + drawList.AddCircleFilled(center, radius, bgColor); + + uint iconColor = style.ToolButtonIconColor; + float th = 2.0f * style.Scale; + float size = radius * 0.5f; + float arm = size * 0.25f; + + // Top Arrow (^) + var topTip = new Vector2(center.X, center.Y - size); + drawList.AddLine(new(topTip.X - arm, topTip.Y + arm), topTip, iconColor, th); + drawList.AddLine(new(topTip.X + arm, topTip.Y + arm), topTip, iconColor, th); + // Bottom Arrow (v) + var botTip = new Vector2(center.X, center.Y + size); + drawList.AddLine(new(botTip.X - arm, botTip.Y - arm), botTip, iconColor, th); + drawList.AddLine(new(botTip.X + arm, botTip.Y - arm), botTip, iconColor, th); + // Left Arrow (<) + var leftTip = new Vector2(center.X - size, center.Y); + drawList.AddLine(new(leftTip.X + arm, leftTip.Y - arm), leftTip, iconColor, th); + drawList.AddLine(new(leftTip.X + arm, leftTip.Y + arm), leftTip, iconColor, th); + // Right Arrow (>) + var rightTip = new Vector2(center.X + size, center.Y); + drawList.AddLine(new(rightTip.X - arm, rightTip.Y - arm), rightTip, iconColor, th); + drawList.AddLine(new(rightTip.X - arm, rightTip.Y + arm), rightTip, iconColor, th); + + return wasModified; + } + } +} \ No newline at end of file diff --git a/Brio/UI/Controls/Sequencer/Memory/AllocationCallbacks.cs b/Brio/UI/Controls/Sequencer/Memory/AllocationCallbacks.cs new file mode 100644 index 00000000..936e1083 --- /dev/null +++ b/Brio/UI/Controls/Sequencer/Memory/AllocationCallbacks.cs @@ -0,0 +1,168 @@ +using System.Runtime.InteropServices; + +namespace ImSequencer.Memory +{ + public unsafe class AllocationCallbacks : IAllocationCallbacks + { +#if TRACELEAK + private readonly List allocations = []; + + public IReadOnlyList Allocations => allocations; +#endif +#if TRACELEAK + + public struct Allocation + { + public void* Ptr; + public nint Size; + public string Caller; + + public Allocation(void* ptr, nint size, string caller) + { + Ptr = ptr; + Size = size; + Caller = caller; + } + } + +#endif + +#if TRACELEAK + + private void Remove(void* ptr) + { + if (ptr == null) + { + return; + } + + lock (allocations) + { + for (int i = 0; i < allocations.Count; i++) + { + Allocation allocation = allocations[i]; + if (allocation.Ptr == ptr) + { + allocations.RemoveAt(i); + return; + } + } + } + } + +#endif + + public void ReportInstances() + { +#if TRACELEAK + lock (allocations) + { + foreach (Allocation allocation in allocations) + { + Logger.Warn($"*** Live allocation ({allocation.Caller}):\n\t{(nint)allocation.Ptr:X}\n\tSize: {allocation.Size}\n"); + } + } +#endif + } + + public void ReportDuplicateInstances() + { +#if TRACELEAK + lock (allocations) + { + foreach (Allocation allocation in allocations) + { + foreach (Allocation allocationCmp in allocations) + { + if (allocation.Caller == allocationCmp.Caller && allocation.Size == allocationCmp.Size) + { + Logger.Warn($"*** Possible duplicate instance ({allocation.Caller}):\n\t{(nint)allocation.Ptr:X}\n\tSize: {allocation.Size}\n"); + } + } + } + } +#endif + } + + public void* Alloc(nuint size +#if TRACELEAK + , string name +#endif + ) + { +#if NET5_0_OR_GREATER + void* ptr = NativeMemory.Alloc(size); +#else + void* ptr = (void*)Marshal.AllocHGlobal((nint)size); +#endif + +#if TRACELEAK + Allocation allocation = new(ptr, size, name); + lock (allocations) + { + allocations.Add(allocation); + } +#endif + return ptr; + } + + public void* ReAlloc(void* ptr, nuint size +#if TRACELEAK + , string name +#endif + ) + { +#if TRACELEAK + Remove(ptr); +#endif +#if NET5_0_OR_GREATER + ptr = NativeMemory.Realloc(ptr, size); +#else + ptr = (void*)Marshal.ReAllocHGlobal((nint)ptr, (nint)size); +#endif +#if TRACELEAK + Allocation allocation = new(ptr, size, name); + lock (allocations) + { + allocations.Add(allocation); + } +#endif + return ptr; + } + + public void Free(void* ptr) + { +#if TRACELEAK + Remove(ptr); +#endif +#if NET5_0_OR_GREATER + NativeMemory.Free(ptr); +#else + Marshal.FreeHGlobal((nint)ptr); +#endif + } + +#if !NETSTANDARD2_1_OR_GREATER && !NET5_0_OR_GREATER + + public unsafe void* Alloc(nint size) + { + return Alloc((nuint)size); + } + + public unsafe void* Alloc(int size) + { + return Alloc((nint)size); + } + + public unsafe void* ReAlloc(void* ptr, nint size) + { + return ReAlloc(ptr, (nuint)size); + } + + public unsafe void* ReAlloc(void* ptr, int size) + { + return ReAlloc(ptr, (nuint)size); + } +#endif + } +} \ No newline at end of file diff --git a/Brio/UI/Controls/Sequencer/Memory/ArrayUtils.cs b/Brio/UI/Controls/Sequencer/Memory/ArrayUtils.cs new file mode 100644 index 00000000..72ce49c5 --- /dev/null +++ b/Brio/UI/Controls/Sequencer/Memory/ArrayUtils.cs @@ -0,0 +1,76 @@ +namespace ImSequencer.Memory +{ + using System; + using System.Collections.Generic; + + /// + /// Utilities for managed and unmanaged arrays. + /// + public static unsafe class ArrayUtils + { + /// + /// Adds a value to an array and resizes it to accommodate the new value. + /// + /// The type of elements in the array. + /// Reference to the array to which the value will be added. + /// The value to add to the array. + public static void Add(ref T[] array, T value) + { + Array.Resize(ref array, array.Length + 1); + array[array.Length - 1] = value; + } + + /// + /// Removes the first occurrence of a value from an array and resizes it accordingly. + /// + /// The type of elements in the array. + /// Reference to the array from which the value will be removed. + /// The value to remove from the array. + public static void Remove(ref T[] array, T value) + { + int index = Array.IndexOf(array, value); + var count = array.Length - index; + Buffer.BlockCopy(array, index + 1, array, index, count); + Array.Resize(ref array, array.Length - 1); + } + + /// + /// Adds a value to a list if it doesn't already exist and returns a boolean indicating success. + /// + /// The type of elements in the list. + /// The list to which the value will be added. + /// The value to add to the list. + /// Returns true if the value was added (not already in the list), otherwise false. + public static bool AddUnique(this IList list, T t) + { + if (list.Contains(t)) + { + return false; + } + + list.Add(t); + + return true; + } + + /// + /// Finds the index of a specified item within an unmanaged array. + /// + /// The type of elements in the array. + /// Pointer to the beginning of the array. + /// Pointer to the item to search for. + /// The number of items in the array. + /// The index of the item if found; otherwise, -1. + public static int IndexOf(T* ptr, T* item, int count) where T : unmanaged + { + for (int i = 0; i < count; i++) + { + if (&ptr[i] == item) + { + return i; + } + } + return -1; + } + } +} diff --git a/Brio/UI/Controls/Sequencer/Memory/IAllocationCallbacks.cs b/Brio/UI/Controls/Sequencer/Memory/IAllocationCallbacks.cs new file mode 100644 index 00000000..28a11aee --- /dev/null +++ b/Brio/UI/Controls/Sequencer/Memory/IAllocationCallbacks.cs @@ -0,0 +1,103 @@ +namespace ImSequencer.Memory +{ + public interface IAllocationCallbacks + { + unsafe void* Alloc(nuint size +#if TRACELEAK + , string name +#endif + ); + +#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER + unsafe void* Alloc(nint size +#if TRACELEAK + , string name +#endif + ) + { + return Alloc((nuint)size +#if TRACELEAK + , name +#endif + ); + } + + unsafe void* Alloc(int size +#if TRACELEAK + , string name +#endif + ) + { + return Alloc((nint)size +#if TRACELEAK + , name +#endif + ); + } +#else + + unsafe void* Alloc(nint size +#if TRACELEAK + , string name +#endif + ); + + unsafe void* Alloc(int size +#if TRACELEAK + , string name +#endif + ); + +#endif + + unsafe void* ReAlloc(void* ptr, nuint size +#if TRACELEAK + , string name +#endif + ); + +#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER + unsafe void* ReAlloc(void* ptr, nint size +#if TRACELEAK + , string name +#endif + ) + { + return ReAlloc(ptr, (nuint)size +#if TRACELEAK + , name +#endif + ); + } + + unsafe void* ReAlloc(void* ptr, int size +#if TRACELEAK + , string name +#endif + ) + { + return ReAlloc(ptr, (nuint)size +#if TRACELEAK + , name +#endif + ); + } +#else + + unsafe void* ReAlloc(void* ptr, nint size +#if TRACELEAK + , string name +#endif +); + + unsafe void* ReAlloc(void* ptr, int size +#if TRACELEAK + , string name +#endif + ); + +#endif + + unsafe void Free(void* ptr); + } +} \ No newline at end of file diff --git a/Brio/UI/Controls/Sequencer/Memory/IConverter1.cs b/Brio/UI/Controls/Sequencer/Memory/IConverter1.cs new file mode 100644 index 00000000..e136146f --- /dev/null +++ b/Brio/UI/Controls/Sequencer/Memory/IConverter1.cs @@ -0,0 +1,7 @@ +namespace ImSequencer.Memory +{ + public interface IConverter + { + public TOut Convert(TIn value); + } +} \ No newline at end of file diff --git a/Brio/UI/Controls/Sequencer/Memory/IFreeable.cs b/Brio/UI/Controls/Sequencer/Memory/IFreeable.cs new file mode 100644 index 00000000..6668367d --- /dev/null +++ b/Brio/UI/Controls/Sequencer/Memory/IFreeable.cs @@ -0,0 +1,13 @@ +namespace ImSequencer.Memory +{ + /// + /// Represents an object that can be released to free associated resources. + /// + public interface IFreeable + { + /// + /// Releases the object and frees any associated resources. + /// + void Release(); + } +} \ No newline at end of file diff --git a/Brio/UI/Controls/Sequencer/Memory/LICENSE b/Brio/UI/Controls/Sequencer/Memory/LICENSE new file mode 100644 index 00000000..2db10eb1 --- /dev/null +++ b/Brio/UI/Controls/Sequencer/Memory/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Juna Meinhold + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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 OR COPYRIGHT HOLDERS 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. \ No newline at end of file diff --git a/Brio/UI/Controls/Sequencer/Memory/README b/Brio/UI/Controls/Sequencer/Memory/README new file mode 100644 index 00000000..5604f2a2 --- /dev/null +++ b/Brio/UI/Controls/Sequencer/Memory/README @@ -0,0 +1 @@ +Code here is reused from https://github.com/HexaEngine/Hexa.NET.Utilities/tree/master and is licensed as such \ No newline at end of file diff --git a/Brio/UI/Controls/Sequencer/Memory/UnsafeList.cs b/Brio/UI/Controls/Sequencer/Memory/UnsafeList.cs new file mode 100644 index 00000000..b78afffa --- /dev/null +++ b/Brio/UI/Controls/Sequencer/Memory/UnsafeList.cs @@ -0,0 +1,659 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using static ImSequencer.Memory.Utils; + +namespace ImSequencer.Memory +{ + /// + /// Represents an unsafe list of elements of type T. + /// + /// The type of the elements. + public unsafe struct UnsafeList : IFreeable, IEnumerable, IList, IReadOnlyList, IEquatable> where T : unmanaged + { + private const int DefaultCapacity = 4; + + private T* items; + private int size; + private int capacity; + + /// + /// Represents an enumerator for the elements in the . + /// + public struct Enumerator : IEnumerator + { + private readonly T* pointer; + private readonly int size; + private int currentIndex; + + /// + /// Initializes a new instance of the struct for the specified . + /// + /// The to enumerate. + internal Enumerator(UnsafeList list) + { + pointer = list.items; + size = list.size; + currentIndex = -1; + } + + /// + /// Gets the element at the current position of the enumerator. + /// + public T Current => pointer[currentIndex]; + + /// + /// Gets the element at the current position of the enumerator. + /// + object IEnumerator.Current => Current; + + /// + /// Disposes of the enumerator. Since the enumerator does not own resources, this method does nothing. + /// + public readonly void Dispose() + { + // Enumerator does not own resources, so nothing to dispose. + } + + /// + /// Moves the enumerator to the next element in the . + /// + /// if the enumerator successfully moved to the next element; if the enumerator has reached the end of the collection. + public bool MoveNext() + { + if (currentIndex < size - 1) + { + currentIndex++; + return true; + } + return false; + } + + /// + /// Resets the enumerator to its initial position, which is before the first element in the . + /// + public void Reset() + { + currentIndex = -1; + } + } + + /// + /// Initializes a new instance of the struct. + /// + public UnsafeList() + { + Capacity = DefaultCapacity; + } + + /// + /// Initializes a new instance of the class with the specified array of values. + /// + /// An array of values to initialize the list. + public UnsafeList(T[] values) + { + Capacity = values.Length; + AppendRange(values); + } + + /// + /// Initializes a new instance of the struct with the specified capacity. + /// + /// The initial capacity of the list. + public UnsafeList(int capacity) + { + Capacity = capacity; + } + + /// + /// Gets the number of elements in the list. + /// + public readonly int Size => size; + + /// + /// Gets the number of elements in the list. + /// + public readonly int Count => size; + + readonly bool ICollection.IsReadOnly => false; + + /// + /// Gets the pointer to the underlying data array. + /// + public readonly T* Data => items; + + /// + /// Gets a value indicating whether the list is empty. + /// + public readonly bool Empty => size == 0; + + /// + /// Gets a pointer to the first element in the list. + /// + public readonly T* Front => items; + + /// + /// Gets a pointer to the last element in the list. + /// + public readonly T* Back => &items[size - 1]; + + /// + /// Gets or sets the capacity of the list. + /// + public int Capacity + { + readonly get => capacity; + set + { + if (items == null) + { + items = AllocT(value); + capacity = value; + Erase(); + return; + } + items = ReAllocT(items, value); + capacity = value; + size = capacity < size ? capacity : size; + } + } + + T IList.this[int index] { get => this[index]; set => this[index] = value; } + + /// + /// Gets or sets the element at the specified index. + /// + public T this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => items[index]; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => items[index] = value; + } + + /// + /// Gets the element at the specified index, with bounds checking. + /// + /// The index of the element to retrieve. + /// The element at the specified index. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T At(int index) + { +#if NET8_0_OR_GREATER + ArgumentOutOfRangeException.ThrowIfNegative(index); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, size); +#else + if (index < 0 || index >= size) + { + throw new ArgumentOutOfRangeException(); + } +#endif + return this[index]; + } + + /// + /// Gets a pointer to the element at the specified index. + /// + /// The index of the element to retrieve. + /// A pointer to the element at the specified index. + public T* GetPointer(int index) + { + return &items[index]; + } + + /// + /// Initializes the list with the default capacity. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Init() + { + Grow(DefaultCapacity); + } + + /// + /// Initializes the list with the specified capacity. + /// + /// The initial capacity of the list. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Init(int capacity) + { + Grow(capacity); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Grow(int capacity) + { + int newcapacity = size == 0 ? DefaultCapacity : 2 * size; + + if (newcapacity < capacity) + { + newcapacity = capacity; + } + + Capacity = newcapacity; + } + + /// + /// Ensures that the list has the specified capacity. + /// + /// The desired capacity. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Reserve(int capacity) + { + if (this.capacity < capacity || items == null) + { + Grow(capacity); + } + } + + /// + /// Reduces the vector's capacity to match its size. + /// + public void ShrinkToFit() + { + Capacity = size; + } + + /// + /// Resizes the vector to the specified size. If the new size is larger than the current capacity, the capacity will be increased accordingly. + /// + /// The new size of the vector. + public void Resize(int newSize) + { + if (size == newSize) + { + return; + } + Reserve(newSize); + size = newSize; + } + + /// + /// Sets all elements in the vector to their default values and resets the size to 0. + /// + public readonly void Erase() + { + ZeroMemoryT(items, capacity); + } + + /// + /// Adds an item to the list. + /// + /// The item to add. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void PushBack(T item) + { + int size = this.size; + if ((uint)size < (uint)capacity) + { + this.size = size + 1; + items[size] = item; + } + else + { + AddWithResize(item); + } + } + + // Non-inline from List.Add to improve its code quality as uncommon path + [MethodImpl(MethodImplOptions.NoInlining)] + private void AddWithResize(T item) + { + int size = this.size; + Grow(size + 1); + this.size = size + 1; + items[size] = item; + } + + /// + /// Adds an element to the end of the vector. + /// + /// The element to add to the vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(T item) + { + PushBack(item); + } + + /// + /// Removes the last element from the vector. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void PopBack() + { + items[size - 1] = default; + size--; + } + + /// + /// Adds a range of items to the list. + /// + /// The array of items to add. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AppendRange(T[] values) + { + Reserve(size + (int)values.Length); + + fixed (T* src = values) + { + Memcpy(src, &items[size], capacity * sizeof(T), values.Length * sizeof(T)); + } + size += values.Length; + } + + /// + /// Appends a range of elements to the end of the vector. + /// + /// A pointer to the array of elements to append. + /// The number of elements to append. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AppendRange(T* values, int count) + { + Reserve(size + count); + + Memcpy(values, &items[size], capacity * sizeof(T), count * sizeof(T)); + + size += count; + } + + /// + /// Removes the specified item from the list. + /// + /// The item to remove. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Remove(T item) + { + var index = IndexOf(item); + if (index == -1) + { + return false; + } + + RemoveAt(index); + return true; + } + + /// + /// Removes the item at the specified index from the list. + /// + /// The index of the item to remove. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RemoveAt(int index) + { + if (index == this.size - 1) + { + items[this.size - 1] = default; + this.size--; + return; + } + + var size = (this.size - index) * sizeof(T); + Buffer.MemoryCopy(&items[index + 1], &items[index], size, size); + this.size--; + } + + /// + /// Inserts an item at the specified index in the list. + /// + /// The index at which to insert the item. + /// The item to insert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Insert(int index, T item) + { + Reserve(this.size + 1); + + var size = (this.size - index) * sizeof(T); + Buffer.MemoryCopy(&items[index], &items[index + 1], size, size); + items[index] = item; + this.size++; + } + + /// + /// Clears the list. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear() + { + size = 0; + } + + /// + /// Determines whether the list contains the specified item. + /// + /// The item to search for. + /// true if the item is found; otherwise, false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(T item) + { + for (int i = 0; i < size; i++) + { + var current = items[i]; + + if (EqualityComparer.Default.Equals(current, item)) + { + return true; + } + } + + return false; + } + + /// + /// Searches for the specified item and returns the index of the first occurrence within the entire list. + /// + /// The item to search for. + /// The index of the first occurrence of the item, or -1 if not found. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int IndexOf(T item) + { + for (int i = 0; i < size; i++) + { + var current = items[i]; + + if (EqualityComparer.Default.Equals(current, item)) + { + return i; + } + } + + return -1; + } + + /// + /// Searches for the first occurrence of an element in the collection that matches the specified condition and returns its index. + /// + /// A function that defines the condition to search for an element. + /// The index of the first element that matches the condition; otherwise, -1 if no match is found. + public int FirstIndexOf(Func comparer) + { + for (int i = 0; i < size; i++) + { + var current = items[i]; + + if (comparer(current)) + { + return i; + } + } + + return -1; + } + + /// + /// Reverses the order of the elements in the list. + /// + public readonly void Reverse() + { + new Span(items, (int)size).Reverse(); + } + + /// + /// Moves the content of another to this list, taking ownership of the memory. + /// + /// The list whose content will be moved to this list. + public void Move(UnsafeList list) + { + Free(items); + items = list.items; + capacity = list.capacity; + size = list.size; + } + +#if NET8_0_OR_GREATER + + /// + /// Atomically increments the counter value and returns the result. + /// + /// The incremented counter value. + /// Note: The capacity remains unchanged due to race conditions. + public int InterlockedIncrementCounter() + { + return Interlocked.Increment(ref size); + } + + /// + /// Atomically decrements the counter value and returns the result. + /// + /// The decremented counter value. + /// Note: The capacity remains unchanged due to race conditions. + public int InterlockedDecrementCounter() + { + return Interlocked.Decrement(ref size); + } + + /// + /// Atomically pushes an element to the end of the list and returns the index of the added element. + /// + /// The value to be added to the list. + /// The index of the added element. + /// Note: The capacity remains unchanged due to race conditions. + public int InterlockedPushBack(T value) + { + int index = Interlocked.Increment(ref size); + items[index] = value; + return index; + } + + /// + /// Atomically removes and returns the index of the last element in the list. + /// + /// The index of the removed element. + /// Note: The capacity remains unchanged due to race conditions. + public int InterlockedPopBack() + { + int index = InterlockedDecrementCounter(); + items[index + 1] = default; + return index; + } + + /// + /// Atomically resizes the list to the specified new size and returns the previous size. + /// + /// The new size to set for the list. + /// The previous size of the list. + /// Note: The capacity remains unchanged due to race conditions. + public int InterlockedResize(int newSize) + { + return Interlocked.Exchange(ref size, newSize); + } + +#endif + + /// + /// Returns an enumerator that iterates through the elements of the . + /// + /// An enumerator for the . + public readonly Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + readonly IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + readonly IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Releases the memory associated with the list. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Release() + { + if (items != null) + { + Free(items); + this = default; + } + } + + void IList.Insert(int index, T item) + { + Insert(index, item); + } + + void IList.RemoveAt(int index) + { + RemoveAt(index); + } + + void ICollection.Add(T item) + { + PushBack(item); + } + + void ICollection.Clear() + { + Clear(); + } + + void ICollection.CopyTo(T[] array, int arrayIndex) + { + fixed (T* dst = array) + { + MemcpyT(&items[arrayIndex], dst, array.Length); + } + } + + bool ICollection.Remove(T item) + { + return Remove(item); + } + + public readonly Span AsSpan() + { + return new(items, (int)size); + } + + public override readonly bool Equals(object? obj) + { + return obj is UnsafeList list && Equals(list); + } + + public readonly bool Equals(UnsafeList other) + { + return items == other.items; + } + + public override readonly int GetHashCode() + { + return ((nint)items).GetHashCode(); + } + + public static bool operator ==(UnsafeList left, UnsafeList right) + { + return left.Equals(right); + } + + public static bool operator !=(UnsafeList left, UnsafeList right) + { + return !(left == right); + } + } +} diff --git a/Brio/UI/Controls/Sequencer/Memory/Utils.cs b/Brio/UI/Controls/Sequencer/Memory/Utils.cs new file mode 100644 index 00000000..c0b3c53d --- /dev/null +++ b/Brio/UI/Controls/Sequencer/Memory/Utils.cs @@ -0,0 +1,1379 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; + +namespace ImSequencer.Memory +{ + /// + /// Utilities for allocation, freeing, reallocating, moving, copying native memory and conversation from managed to unmanaged. + /// + public static unsafe partial class Utils + { + private static IAllocationCallbacks allocator = new AllocationCallbacks(); + + public static IAllocationCallbacks Allocator { get => allocator; set => allocator = value; } + + public const int StackAllocLimit = 2048; + + /// + /// Swaps the memory content between two pointers of the specified size. + /// + /// Pointer to the first memory location. + /// Pointer to the second memory location. + /// Size of each element in bytes. + public static unsafe void Swap(void* a, void* b, int size) + { + byte* byteA = (byte*)a; + byte* byteB = (byte*)b; + + for (int i = 0; i < size; i++) + { + (byteB[i], byteA[i]) = (byteA[i], byteB[i]); + } + } + + /// + /// Swaps the memory content between two pointers of the specified size. + /// + /// Pointer to the first memory location. + /// Pointer to the second memory location. + public static unsafe void Swap(T* a, T* b) where T : unmanaged + { + (*b, *a) = (*a, *b); + } + + /// + /// Converts a UTF-8 encoded null-terminated byte pointer to a managed string. + /// + /// The pointer to the UTF-8 encoded string. + /// A managed string representing the UTF-8 data. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string? ToStringFromUTF8(byte* ptr) + { + return new string((sbyte*)ptr); + } + + /// + /// Counts the number of set bits (1s) in the binary representation of an unsigned integer. + /// + /// The unsigned integer to count bits in. + /// The count of set bits in the binary representation of the integer. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Bitcount(this uint value) + { + uint v = value; + v -= v >> 1 & 0x55555555; // reuse input as temporary + v = (v & 0x33333333) + (v >> 2 & 0x33333333); // temp + uint c = (v + (v >> 4) & 0xF0F0F0F) * 0x1010101 >> 24; // count + return c; + } + + /// + /// Swaps the values of two variables. + /// + /// The type of the variables. + /// The first variable. + /// The second variable. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Swap(ref T a, ref T b) + { + (b, a) = (a, b); + } + + /// + /// Converts to utf16 pointer. + /// + /// The string. + /// The pointer, must be freed after usage. + public static char* ToUTF16Ptr(this string str) + { + char* dst = AllocT(str.Length + 1); + fixed (char* src = str) + { + MemcpyT(src, dst, str.Length, str.Length); + } + dst[str.Length] = '\0'; + return dst; + } + + /// + /// Converts an array to an native pointer. + /// + /// + /// The values. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T* ToPtr(this T[] values) where T : unmanaged + { + uint bytesToCopy = (uint)values.Length * (uint)sizeof(T); + T* result = (T*)Alloc(bytesToCopy); + fixed (T* src = values) + { + MemcpyT(src, result, bytesToCopy, bytesToCopy); + } + return result; + } + + /// + /// Converts to utf8 pointer. + /// + /// The string. + /// The pointer, must be freed after usage. + public static byte* ToUTF8Ptr(this string str) + { + var byteCount = Encoding.UTF8.GetByteCount(str); + byte* dst = AllocT(byteCount + 1); + fixed (char* src = str) + { + Encoding.UTF8.GetBytes(src, str.Length, dst, byteCount); + } + dst[str.Length] = 0; + return dst; + } + + /// + /// Copies memory from the source to the destination with specified lengths. + /// + /// Pointer to the source memory. + /// Pointer to the destination memory. + /// Length of the destination memory to copy to. + /// Length of the source memory to copy from. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Memcpy(void* src, void* dst, uint dstLength, uint srcLength) + { + Buffer.MemoryCopy(src, dst, dstLength, srcLength); + } + + /// + /// Copies memory from the source to the destination with specified lengths. + /// + /// Pointer to the source memory. + /// Pointer to the destination memory. + /// Length of the destination memory to copy to. + /// Length of the source memory to copy from. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Memcpy(void* src, void* dst, int dstLength, int srcLength) + { + Buffer.MemoryCopy(src, dst, dstLength, srcLength); + } + + /// + /// Copies memory from the source to the destination with specified lengths. + /// + /// Pointer to the source memory. + /// Pointer to the destination memory. + /// Length of the destination memory to copy to. + /// Length of the source memory to copy from. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Memcpy(void* src, void* dst, long dstLength, long srcLength) + { + Buffer.MemoryCopy(src, dst, dstLength, srcLength); + } + + /// + /// Copies memory from the source to the destination with specified lengths. + /// + /// Pointer to the source memory. + /// Pointer to the destination memory. + /// Length of the destination memory to copy to. + /// Length of the source memory to copy from. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Memcpy(void* src, void* dst, ulong dstLength, ulong srcLength) + { + Buffer.MemoryCopy(src, dst, dstLength, srcLength); + } + + /// + /// Copies memory from the source to the destination with the same length for both source and destination. + /// + /// Pointer to the source memory. + /// Pointer to the destination memory. + /// Length of the source and destination memory to copy. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Memcpy(void* src, void* dst, uint length) + { + Buffer.MemoryCopy(src, dst, length, length); + } + + /// + /// Copies memory from the source to the destination with the same length for both source and destination. + /// + /// Pointer to the source memory. + /// Pointer to the destination memory. + /// Length of the source and destination memory to copy. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Memcpy(void* src, void* dst, int length) + { + Buffer.MemoryCopy(src, dst, length, length); + } + + /// + /// Copies memory from the source to the destination with the same length for both source and destination. + /// + /// Pointer to the source memory. + /// Pointer to the destination memory. + /// Length of the source and destination memory to copy. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Memcpy(void* src, void* dst, long length) + { + Buffer.MemoryCopy(src, dst, length, length); + } + + /// + /// Copies memory from the source to the destination with the same length for both source and destination. + /// + /// Pointer to the source memory. + /// Pointer to the destination memory. + /// Length of the source and destination memory to copy. + public static void Memcpy(void* src, void* dst, ulong length) + { + Buffer.MemoryCopy(src, dst, length, length); + } + + /// + /// Copies memory from the source to the destination with the same length for both source and destination. + /// + /// Pointer to the source memory. + /// Pointer to the destination memory. + /// Length of the destination memory to copy to. + /// Length of the source memory to copy from. + /// Type of elements to copy. + public static void MemcpyT(T* src, T* dst, uint dstLength, uint srcLength) where T : unmanaged + { + Buffer.MemoryCopy(src, dst, dstLength * sizeof(T), srcLength * sizeof(T)); + } + + /// + /// Copies memory from the source to the destination with the same length for both source and destination. + /// + /// Pointer to the source memory. + /// Pointer to the destination memory. + /// Length of the destination memory to copy to. + /// Length of the source memory to copy from. + /// Type of elements to copy. + public static void MemcpyT(T* src, T* dst, int dstLength, int srcLength) where T : unmanaged + { + Buffer.MemoryCopy(src, dst, dstLength * sizeof(T), srcLength * sizeof(T)); + } + + /// + /// Copies memory from the source to the destination with the same length for both source and destination. + /// + /// Pointer to the source memory. + /// Pointer to the destination memory. + /// Length of the destination memory to copy to. + /// Length of the source memory to copy from. + /// Type of elements to copy. + public static void MemcpyT(T* src, T* dst, nint dstLength, nint srcLength) where T : unmanaged + { + Buffer.MemoryCopy(src, dst, dstLength * sizeof(T), srcLength * sizeof(T)); + } + + /// + /// Copies memory from the source to the destination with the same length for both source and destination. + /// + /// Pointer to the source memory. + /// Pointer to the destination memory. + /// Length of the source and destination memory to copy. + /// Type of elements to copy. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MemcpyT(T* src, T* dst, uint length) where T : unmanaged + { + Buffer.MemoryCopy(src, dst, length * sizeof(T), length * sizeof(T)); + } + + /// + /// Copies memory from the source to the destination with the same length for both source and destination. + /// + /// Pointer to the source memory. + /// Pointer to the destination memory. + /// Length of the source and destination memory to copy. + /// Type of elements to copy. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MemcpyT(T* src, T* dst, int length) where T : unmanaged + { + Buffer.MemoryCopy(src, dst, length * sizeof(T), length * sizeof(T)); + } + + /// + /// Sets all bytes in memory to zero for a span of type T. + /// + /// The type of elements in the span. + /// Pointer to the memory to clear. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ZeroMemoryT(T* pointer) where T : unmanaged + { + Unsafe.InitBlockUnaligned(pointer, 0, (uint)sizeof(T)); + } + + /// + /// Sets all bytes in memory to zero for a span of type T. + /// + /// The type of elements in the span. + /// Pointer to the memory to clear. + /// Number of elements of type T to clear. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ZeroMemoryT(T* pointer, int length) where T : unmanaged + { + ZeroMemory(pointer, sizeof(T) * length); + } + + /// + /// Sets all bytes in memory to zero for a span of type T. + /// + /// The type of elements in the span. + /// Pointer to the memory to clear. + /// Number of elements of type T to clear. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ZeroMemoryT(T* pointer, uint length) where T : unmanaged + { + ZeroMemory(pointer, sizeof(T) * (int)length); + } + + /// + /// Sets all bytes in memory to zero for a span of type T. + /// + /// The type of elements in the span. + /// Pointer to the memory to clear. + /// Number of elements of type T to clear. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ZeroMemoryT(T* pointer, nint length) where T : unmanaged + { + ZeroMemory(pointer, sizeof(T) * length); + } + + /// + /// Sets all bytes in memory to zero for a specified number of bytes. + /// + /// Pointer to the memory to clear. + /// Number of bytes to clear. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ZeroMemory(void* pointer, uint size) + { + Unsafe.InitBlockUnaligned(pointer, 0, size); + } + + /// + /// Sets all bytes in memory to zero for a specified number of bytes. + /// + /// Pointer to the memory to clear. + /// Number of bytes to clear. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ZeroMemory(void* pointer, int size) + { + Unsafe.InitBlockUnaligned(pointer, 0, (uint)size); + } + + /// + /// Sets all bytes in memory to zero for a specified number of bytes. + /// + /// Pointer to the memory to clear. + /// Number of bytes to clear. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ZeroMemory(void* pointer, ulong size) + { + while (size > uint.MaxValue) + { + Unsafe.InitBlockUnaligned(pointer, 0, uint.MaxValue); + size -= uint.MaxValue; + } + + if (size > 0) + { + Unsafe.InitBlockUnaligned(pointer, 0, (uint)size); + } + } + + /// + /// Sets all bytes in memory to zero for a specified number of bytes. + /// + /// Pointer to the memory to clear. + /// Number of bytes to clear. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ZeroMemory(void* pointer, long size) + { + while (size > uint.MaxValue) + { + Unsafe.InitBlockUnaligned(pointer, 0, uint.MaxValue); + size -= uint.MaxValue; + } + + if (size > 0) + { + Unsafe.InitBlockUnaligned(pointer, 0, (uint)size); + } + } + + /// + /// Sets all bytes in memory to zero for a specified number of bytes. + /// + /// Pointer to the memory to clear. + /// Number of bytes to clear. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ZeroMemory(void* pointer, nint size) + { + if (sizeof(nint) == 8) + { + ZeroMemory(pointer, (long)size); + } + else + { + ZeroMemory(pointer, (int)size); + } + } + + /// + /// Allocates and returns a pointer to memory for a single element of type T, initializing it with the provided data. + /// + /// The type of the element to allocate. + /// The data to initialize the allocated memory with. + /// A pointer to the allocated memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T* AllocT(T data +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) where T : unmanaged + { + T* result = (T*)allocator.Alloc(sizeof(T) +#if TRACELEAK + , $"File: {file}, Line: {line}" +#endif + ); + *result = data; + return result; + } + + /// + /// Allocates and returns a pointer to memory for a single element of type T, initializing it with the default value. + /// + /// The type of the element to allocate. + /// A pointer to the allocated memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T* AllocT( +#if TRACELEAK + [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) where T : unmanaged + { + T* result = (T*)allocator.Alloc(sizeof(T) +#if TRACELEAK + , $"File: {file}, Line: {line}" +#endif + ); + *result = new T(); + return result; + } + + /// + /// Allocates and returns a pointer to memory for an array of elements of type T. + /// + /// The type of the elements to allocate. + /// The number of elements to allocate. + /// A pointer to the allocated memory for the array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T* AllocT(int count +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) where T : unmanaged + { + T* result = (T*)allocator.Alloc(sizeof(T) * count +#if TRACELEAK + , $"File: {file}, Line: {line}" +#endif + ); + return result; + } + + /// + /// Allocates and returns a pointer to memory for an array of elements of type T. + /// + /// The type of the elements to allocate. + /// The number of elements to allocate. + /// A pointer to the allocated memory for the array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T* AllocT(uint count +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) where T : unmanaged + { + T* result = (T*)allocator.Alloc((nint)(sizeof(T) * count) +#if TRACELEAK + , $"File: {file}, Line: {line}" +#endif + ); + return result; + } + + /// + /// Allocates and returns a pointer to memory for an array of elements of type T. + /// + /// The type of the elements to allocate. + /// The number of elements to allocate. + /// A pointer to the allocated memory for the array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T* AllocT(nint count +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) where T : unmanaged + { + T* result = (T*)allocator.Alloc(sizeof(T) * count +#if TRACELEAK + , $"File: {file}, Line: {line}" +#endif + ); + return result; + } + + /// + /// Allocates and returns a pointer to memory for an array of elements of type T. + /// + /// The type of the elements to allocate. + /// The number of elements to allocate. + /// A pointer to the allocated memory for the array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T* AllocT(nuint count +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) where T : unmanaged + { + T* result = (T*)allocator.Alloc((nuint)sizeof(T) * count +#if TRACELEAK + , $"File: {file}, Line: {line}" +#endif + ); + return result; + } + + /// + /// Allocates and returns a pointer to unmanaged memory. + /// + /// The number of bytes to allocate. + /// A pointer to the allocated memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void* Alloc(nint count +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) + { + void* result = allocator.Alloc(count +#if TRACELEAK + , $"File: {file}, Line: {line}" +#endif + ); + return result; + } + + /// + /// Allocates and returns a pointer to unmanaged memory. + /// + /// The number of bytes to allocate. + /// A pointer to the allocated memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void* Alloc(nuint count +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) + { + void* result = allocator.Alloc((nint)count +#if TRACELEAK + , $"File: {file}, Line: {line}" +#endif + ); + return result; + } + + /// + /// Allocates and returns a pointer to unmanaged memory. + /// + /// The size, in bytes, to allocate. + /// A pointer to the allocated memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void* Alloc(int size +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) + { + void* result = allocator.Alloc(size +#if TRACELEAK + , $"File: {file}, Line: {line}" +#endif + ); + return result; + } + + /// + /// Allocates and returns a pointer to unmanaged memory. + /// + /// The size, in bytes, to allocate. + /// A pointer to the allocated memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void* Alloc(uint size +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) + { + void* result = allocator.Alloc((nint)size +#if TRACELEAK + , $"File: {file}, Line: {line}" +#endif + ); + return result; + } + + /// + /// Reallocates and returns a pointer to unmanaged memory for an array of elements of type T, preserving the existing data. + /// + /// The type of the elements in the array. + /// A pointer to the existing memory. + /// The new number of elements to allocate. + /// A pointer to the reallocated memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T* ReAllocT(T* pointer, int count +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) where T : unmanaged + { + T* result = (T*)allocator.ReAlloc(pointer, count * sizeof(T) +#if TRACELEAK + , $"File: {file}, Line: {line}" +#endif + ); + return result; + } + + /// + /// Reallocates and returns a pointer to unmanaged memory for an array of elements of type T, preserving the existing data. + /// + /// The type of the elements in the array. + /// A pointer to the existing memory. + /// The new number of elements to allocate. + /// A pointer to the reallocated memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T* ReAllocT(T* pointer, uint count +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) where T : unmanaged + { + T* result = (T*)allocator.ReAlloc(pointer, (nint)(count * sizeof(T)) +#if TRACELEAK + , $"File: {file}, Line: {line}" +#endif + ); + return result; + } + + /// + /// Reallocates and returns a pointer to unmanaged memory for an array of elements of type T, preserving the existing data. + /// + /// The type of the elements in the array. + /// A pointer to the existing memory. + /// The new number of elements to allocate. + /// A pointer to the reallocated memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T* ReAllocT(T* pointer, nint count +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) where T : unmanaged + { + T* result = (T*)allocator.ReAlloc(pointer, count * sizeof(T) +#if TRACELEAK + , $"File: {file}, Line: {line}" +#endif + ); + return result; + } + + /// + /// Reallocates and returns a pointer to unmanaged memory for an array of elements of type T, preserving the existing data. + /// + /// The type of the elements in the array. + /// A pointer to the existing memory. + /// The new number of elements to allocate. + /// A pointer to the reallocated memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T* ReAllocT(T* pointer, nuint count +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) where T : unmanaged + { + T* result = (T*)allocator.ReAlloc(pointer, count * (nuint)sizeof(T) +#if TRACELEAK + , $"File: {file}, Line: {line}" +#endif + ); + return result; + } + + /// + /// Reallocates and returns a pointer to unmanaged memory, preserving the existing data. + /// + /// A pointer to the existing memory. + /// The new number of bytes to allocate. + /// A pointer to the reallocated memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void* ReAlloc(void* pointer, int count +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) + { + void* result = allocator.ReAlloc(pointer, count +#if TRACELEAK + , $"File: {file}, Line: {line}" +#endif + ); + return result; + } + + /// + /// Reallocates and returns a pointer to unmanaged memory, preserving the existing data. + /// + /// A pointer to the existing memory. + /// The new number of bytes to allocate. + /// A pointer to the reallocated memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void* ReAlloc(void* pointer, uint count +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) + { + void* result = allocator.ReAlloc(pointer, (nint)count +#if TRACELEAK + , $"File: {file}, Line: {line}" +#endif + ); + return result; + } + + /// + /// Reallocates and returns a pointer to unmanaged memory, preserving the existing data. + /// + /// A pointer to the existing memory. + /// The new number of bytes to allocate. + /// A pointer to the reallocated memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void* ReAlloc(void* pointer, nint count +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) + { + void* result = allocator.ReAlloc(pointer, count +#if TRACELEAK + , $"File: {file}, Line: {line}" +#endif + ); + return result; + } + + /// + /// Allocates memory for an array of elements of type T, copies data from the source pointer to the new memory, and returns a pointer to the new memory. + /// + /// The type of the elements in the array. + /// A pointer to the source data to be copied. + /// The number of elements in the array. + /// A pointer to the newly allocated memory with the copied data. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T* AllocCopyT(T* pointer, int length +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) where T : unmanaged + { + T* result = AllocT(length +#if TRACELEAK + , file, line +#endif + ); + MemcpyT(pointer, result, length, length); + return result; + } + + /// + /// Allocates memory for an array of elements of type T, copies data from the source pointer to the new memory, and returns a pointer to the new memory. + /// + /// The type of the elements in the array. + /// A pointer to the source data to be copied. + /// The number of elements in the array. + /// A pointer to the newly allocated memory with the copied data. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T* AllocCopyT(T* pointer, uint length +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) where T : unmanaged + { + T* result = AllocT(length +#if TRACELEAK + , file, line +#endif + ); + MemcpyT(pointer, result, length, length); + return result; + } + + /// + /// Allocates memory for an array of elements of type T, copies data from the source span to the new memory, and returns a pointer to the new memory. + /// + /// The type of the elements in the array. + /// The source span to copy from. + /// A pointer to the newly allocated memory with the copied data. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T* AllocCopyT(T[] source +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) where T : unmanaged + { + int length = source.Length; + T* result = AllocT(length +#if TRACELEAK + , file, line +#endif + ); + fixed (T* pointer = source) + { + MemcpyT(pointer, result, length, length); + } + + return result; + } + + /// + /// Allocates memory for an array of elements of type T, copies data from the source span to the new memory, and returns a pointer to the new memory. + /// + /// The type of the elements in the array. + /// The source span to copy from. + /// A pointer to the newly allocated memory with the copied data. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T* AllocCopyT(Span source +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) where T : unmanaged + { + int length = source.Length; + T* result = AllocT(length +#if TRACELEAK + , file, line +#endif + ); + for (int i = 0; i < length; i++) + { + result[i] = source[i]; + } + + return result; + } + + /// + /// Allocates memory for an array of elements of type T, copies data from the source span to the new memory, and returns a pointer to the new memory. + /// + /// The type of the elements in the array. + /// The source span to copy from. + /// A pointer to the newly allocated memory with the copied data. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T* AllocCopyT(ReadOnlySpan source +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) where T : unmanaged + { + int length = source.Length; + T* result = AllocT(length +#if TRACELEAK + , file, line +#endif + ); + for (int i = 0; i < length; i++) + { + result[i] = source[i]; + } + + return result; + } + + /// + /// Allocates memory for an array of bytes, copies data from the source pointer to the new memory, and returns a pointer to the new memory. + /// + /// A pointer to the source data to be copied. + /// The number of bytes to allocate and copy. + /// A pointer to the newly allocated memory with the copied data. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void* AllocCopy(void* pointer, int length +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) + { + void* result = Alloc(length +#if TRACELEAK + , file, line +#endif + ); + Memcpy(pointer, result, length, length); + return result; + } + + /// + /// Allocates memory for an array of bytes, copies data from the source pointer to the new memory, and returns a pointer to the new memory. + /// + /// A pointer to the source data to be copied. + /// The number of bytes to allocate and copy. + /// A pointer to the newly allocated memory with the copied data. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void* AllocCopy(void* pointer, uint length +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) + { + void* result = Alloc(length); + Memcpy(pointer, result, length, length); + return result; + } + + /// + /// Allocates memory for an array of bytes, copies data from the source pointer to the new memory, and returns a pointer to the new memory. + /// + /// A pointer to the source data to be copied. + /// The number of bytes to allocate and copy. + /// A pointer to the newly allocated memory with the copied data. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void* AllocCopy(void* pointer, nint length +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) + { + void* result = Alloc(length +#if TRACELEAK + , file, line +#endif + ); + Memcpy(pointer, result, length, length); + return result; + } + + /// + /// Allocates memory for an array of bytes, copies data from the source pointer to the new memory, and returns a pointer to the new memory. + /// + /// A pointer to the source data to be copied. + /// The number of bytes to allocate and copy. + /// A pointer to the newly allocated memory with the copied data. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void* AllocCopy(void* pointer, nuint length +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) + { + void* result = Alloc(length +#if TRACELEAK + , file, line +#endif + ); + Memcpy(pointer, result, length, length); + return result; + } + + /// + /// Allocates memory for an array of pointers and returns a pointer to the newly allocated memory. + /// + /// The number of pointers to allocate. + /// A pointer to the newly allocated memory for an array of pointers. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void** AllocArray(uint length +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) + { + void** result = (void**)allocator.Alloc((nint)(length * sizeof(void*)) +#if TRACELEAK + , $"File: {file}, Line: {line}" +#endif + ); + return result; + } + + /// + /// Allocates memory for an array of pointers and returns a pointer to the newly allocated memory. + /// + /// The number of pointers to allocate. + /// A pointer to the newly allocated memory for an array of pointers. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T** AllocArrayT(uint length +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) where T : unmanaged + { + T** result = (T**)allocator.Alloc((nint)(length * sizeof(void*)) +#if TRACELEAK + , $"File: {file}, Line: {line}" +#endif + ); + return result; + } + + /// + /// Allocates memory for an array of pointers and returns a pointer to the newly allocated memory. + /// + /// The number of pointers to allocate. + /// A pointer to the newly allocated memory for an array of pointers. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T** AllocArrayT(int length +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) where T : unmanaged + { + T** result = (T**)allocator.Alloc((nint)(length * sizeof(void*)) +#if TRACELEAK + , $"File: {file}, Line: {line}" +#endif + ); + return result; + } + + /// + /// Allocates memory for an array of pointers, sets the memory to zero, and returns a pointer to the newly allocated memory. + /// + /// The number of pointers to allocate and set to zero. + /// A pointer to the newly allocated memory for an array of pointers with zeroed memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void** AllocArrayAndZero(uint length +#if TRACELEAK + , [CallerFilePath] string file = "", [CallerLineNumber] int line = 0 +#endif + ) + { + var result = AllocArray(length +#if TRACELEAK + , file, line +#endif + ); + ZeroMemory(result, (int)(sizeof(nint) * length)); + return result; + } + + /// + /// Frees memory allocated for an unmanaged resource associated with a pointer to a type that implements . + /// + /// The unmanaged type that implements . + /// A pointer to the unmanaged resource to be released. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Free(T* pointer) where T : unmanaged, IFreeable + { + pointer->Release(); + allocator.Free(pointer); + } + + /// + /// Frees memory allocated for an unmanaged resource associated with a pointer. + /// + /// A pointer to the unmanaged resource to be released. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Free(void* pointer) + { + allocator.Free(pointer); + } + + /// + /// Frees memory allocated for an array of pointers. + /// + /// A pointer to the array of pointers to be released. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Free(void** pointer) + { + allocator.Free(pointer); + } + + /// + /// Copies unmanaged memory to a new managed array. + /// + /// The unmanaged type of elements. + /// A pointer to the source unmanaged memory. + /// The number of elements to copy. + /// A new managed array containing the copied elements or null if is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T[]? ToManaged(T* src, int length) where T : unmanaged + { + if (src == null) + { + return null; + } + + T[] values = new T[length]; + fixed (T* dst = values) + { + MemcpyT(src, dst, length, length); + } + return values; + } + + /// + /// Copies unmanaged memory to a new managed array. + /// + /// The unmanaged type of elements. + /// A pointer to the source unmanaged memory. + /// The number of elements to copy. + /// A new managed array containing the copied elements or null if is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T[]? ToManaged(T* src, uint length) where T : unmanaged + { + if (src == null) + { + return null; + } + + T[] values = new T[length]; + fixed (T* dst = values) + { + MemcpyT(src, dst, length, length); + } + return values; + } + + /// + /// Sets the memory at the specified pointer to the specified value for a given number of elements. + /// + /// The unmanaged type of elements. + /// A pointer to the memory to set. + /// The byte value to set the memory to. + /// The number of elements to set. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MemsetT(T* ptr, T value, int count) where T : unmanaged + { + for (int i = 0; i < count; i++) + { + ptr[i] = value; + } + } + + /// + /// Sets the memory at the specified pointer to the specified value for a given number of elements. + /// + /// The unmanaged type of elements. + /// A pointer to the memory to set. + /// The byte value to set the memory to. + /// The number of elements to set. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MemsetT(T* ptr, byte value, int count) where T : unmanaged + { + new Span(ptr, count * sizeof(T)).Fill(value); + } + + /// + /// Sets the memory at the specified pointer to the specified value for a given number of elements. + /// + /// The unmanaged type of elements. + /// A pointer to the memory to set. + /// The byte value to set the memory to. + /// The number of elements to set. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MemsetT(T* ptr, byte value, uint count) where T : unmanaged + { + new Span(ptr, (int)(count * sizeof(T))).Fill(value); + } + + /// + /// Sets the memory at the specified pointer to the specified value for a given number of bytes. + /// + /// A pointer to the memory to set. + /// The byte value to set the memory to. + /// The number of bytes to set. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Memset(void* ptr, byte value, int length) + { + Unsafe.InitBlockUnaligned(ptr, value, (uint)length); + } + + /// + /// Sets the memory at the specified pointer to the specified value for a given number of bytes. + /// + /// A pointer to the memory to set. + /// The byte value to set the memory to. + /// The number of bytes to set. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Memset(void* ptr, byte value, uint length) + { + Unsafe.InitBlockUnaligned(ptr, value, length); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Contains(T* data, int size, T* str, int length) where T : unmanaged, IEquatable + { + int cmp = 0; + for (int i = 0; i < size; i++) + { + if (data[i].Equals(str[cmp])) + { + cmp++; + if (cmp == length) + { + return true; + } + } + else + { + cmp = 0; + } + } + + return cmp == length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Contains(TSrc* data, int size, TDst* str, int length, Func convert) where TSrc : unmanaged, IEquatable where TDst : unmanaged + { + if (length > size) + { + return false; + } + + int cmp = 0; + for (int i = 0; i < size; i++) + { + if (data[i].Equals(convert(str[cmp]))) + { + cmp++; + if (cmp == length) + { + return true; + } + } + else + { + cmp = 0; + } + } + + return cmp == length; + } + + public static int Find(T* data, int size, T* str, int length, int pos) where T : unmanaged, IEquatable + { + if (length > size - pos) + { + return -1; + } + + int cmp = 0; + for (int i = pos; i < size; i++) + { + if (data[i].Equals(str[cmp])) + { + cmp++; + if (cmp == length) + { + return i - cmp + 1; + } + } + else + { + cmp = 0; + } + } + + return -1; + } + + public static int Find(T* data, int size, T* str, int length, int pos, IEqualityComparer comparer) where T : unmanaged, IEquatable + { + if (length > size - pos) + { + return -1; + } + + int cmp = 0; + for (int i = pos; i < size; i++) + { + if (comparer.Equals(data[i], str[cmp])) + { + cmp++; + if (cmp == length) + { + return i - cmp + 1; + } + } + else + { + cmp = 0; + } + } + + return -1; + } + + public static int Find(TSrc* data, int size, TDst* str, int length, int pos, IConverter convert) where TSrc : unmanaged, IEquatable where TDst : unmanaged + { + if (length > size - pos) + { + return -1; + } + + int cmp = 0; + for (int i = pos; i < size; i++) + { + if (data[i].Equals(convert.Convert(str[cmp]))) + { + cmp++; + if (cmp == length) + { + return i - cmp + 1; + } + } + else + { + cmp = 0; + } + } + + return -1; + } + + public static int Find(TSrc* data, int size, TDst* str, int length, int pos, IConverter convert, IEqualityComparer comparer) where TSrc : unmanaged, IEquatable where TDst : unmanaged + { + if (length > size - pos) + { + return -1; + } + + int cmp = 0; + for (int i = pos; i < size; i++) + { + if (comparer.Equals(data[i], convert.Convert(str[cmp]))) + { + cmp++; + if (cmp == length) + { + return i - cmp + 1; + } + } + else + { + cmp = 0; + } + } + + return -1; + } + } +} diff --git a/Brio/UI/Controls/Sequencer/SequenceInterface.cs b/Brio/UI/Controls/Sequencer/SequenceInterface.cs new file mode 100644 index 00000000..c4e34859 --- /dev/null +++ b/Brio/UI/Controls/Sequencer/SequenceInterface.cs @@ -0,0 +1,27 @@ +using Dalamud.Bindings.ImGui; + +namespace ImSequencer; + +public unsafe interface SequenceInterface +{ + bool focused { get; set; } + int GetFrameMin(); + int GetFrameMax(); + int GetItemCount(); + void BeginEdit(int index); + void EndEdit(); + int GetItemTypeCount(); + string GetItemTypeName(int index); + string GetItemLabel(int index); + string GetCollapseFmt(int frameCount, int sequenceCount); + void Get(int index, int** start, int** end, int* type, uint* color); + void Add(int index); + void Del(int index); + void Duplicate(int index); + void Copy(); + void Paste(); + uint GetCustomHeight(int index); + void DoubleClick(int index); + void CustomDraw(int index, ImDrawListPtr drawList, ImRect customRect, ImRect legendRect, ImRect clippingRect, ImRect legendClippingRect); + void CustomDrawCompact(int index, ImDrawListPtr drawList, ImRect customRect, ImRect clippingRect); +} diff --git a/Brio/UI/Controls/Sequencer/Sequencer.cs b/Brio/UI/Controls/Sequencer/Sequencer.cs new file mode 100644 index 00000000..b3a99558 --- /dev/null +++ b/Brio/UI/Controls/Sequencer/Sequencer.cs @@ -0,0 +1,559 @@ +using System; +using System.Collections.Generic; +using Dalamud.Bindings.ImGui; +using System.Numerics; + + +namespace ImSequencer +{ + + public static class ImSequencer + { + private static SequencerCustomDraw _sequencerCustomDraw; + + private static bool SequencerAddDelButton(ImDrawListPtr draw_list, Vector2 pos, bool add = true) + { + + ImGuiIOPtr io = ImGui.GetIO(); + ImRect btnRect = new ImRect(pos, new Vector2(pos.X + 16, pos.Y + 16)); + bool overBtn = btnRect.Contains(io.MousePos); + bool containedClick = overBtn && btnRect.Contains(io.MouseClickedPos[0]); + bool clickedBtn = containedClick && io.MouseReleased[0]; + uint btnColor = overBtn ? 0xAAEAFFAA : 0x77A3B2AA; + if (containedClick && io.MouseDownDuration[0] > 0) + btnRect.Expand(2.0f); + + float midy = pos.Y + 16 / 2 - 0.5f; + float midx = pos.X + 16 / 2 - 0.5f; + draw_list.AddRect(btnRect.Min, btnRect.Max,(uint) btnColor, 4); + draw_list.AddLine(new Vector2(btnRect.Min.X + 3, midy), new Vector2(btnRect.Max.X - 3, midy), (uint)btnColor, 2f); + if (add) + draw_list.AddLine(new Vector2(midx, btnRect.Min.Y + 3), new Vector2(midx, btnRect.Max.Y - 3), (uint) btnColor, 2f); + return clickedBtn; + } + + private static SeqContext.SequencerContext ctx = new(); + public static unsafe bool Sequencer(SequenceInterface sequence, ref int currentFrame, ref bool expanded, ref int selectedEntry, ref int firstFrame, SEQUENCER_OPTIONS sequenceOptions) + { + bool ret = false; + ImGuiIOPtr io = ImGui.GetIO(); + int cx = (int)(io.MousePos.X); + int cy = (int)(io.MousePos.Y); + bool popupOpened = false; + int sequenceCount = sequence.GetItemCount(); + if (sequenceCount == 0) + return false; + ImGui.BeginGroup(); + ImDrawListPtr draw_list = ImGui.GetWindowDrawList(); + Vector2 canvas_pos = ImGui.GetCursorScreenPos(); + Vector2 canvas_size = ImGui.GetContentRegionAvail(); + int firstFrameUsed = firstFrame; + + int controlHeight = sequenceCount * ctx.ItemHeight; + for (int i = 0; i < sequenceCount; i++) + controlHeight += (int)sequence.GetCustomHeight(i); + int frameCount = Math.Max(sequence.GetFrameMax() - sequence.GetFrameMin(), 1); + var viewWidthPixels = canvas_size.X - ctx.legendWidth; + + + + var customDraws = new List(); + var compactCustomDraws = new List(); + // zoom in/out + int visibleFrameCount = (int)Math.Floor((canvas_size.X - ctx.legendWidth) / ctx.framePixelWidth); + float barWidthRatio = Math.Min((float)visibleFrameCount / (float)frameCount, 1f); + float barWidthInPixels = barWidthRatio * (canvas_size.X - ctx.legendWidth); + + ImRect regionRect = new ImRect(canvas_pos, Extensions.Add(canvas_pos, canvas_size)); + + + ctx.PanningViewSource = new Vector2(); + ctx.PanningViewFrame = 0; + if (ImGui.IsWindowFocused() && io.KeyAlt && io.MouseDown[2]) + { + if (!ctx.PanningView) + { + ctx.PanningViewSource = io.MousePos; + ctx.PanningView = true; + ctx.PanningViewFrame = firstFrame; + } + firstFrame = ctx.PanningViewFrame - (int)((io.MousePos.X - ctx.PanningViewSource.X) / ctx.framePixelWidth); + firstFrame = Math.Clamp(firstFrame, sequence.GetFrameMin(), sequence.GetFrameMax() - visibleFrameCount); + } + if (ctx.PanningView && !io.MouseDown[2]) + { + ctx.PanningView = false; + } + ctx.ScrollbarState.ContentMin = sequence.GetFrameMin(); + ctx.ScrollbarState.ContentMax = sequence.GetFrameMax(); + + if (ctx.ScrollbarState.MinViewSpan <= 0) + ctx.ScrollbarState.MinViewSpan = 10; + + if (ctx.ScrollbarState.ViewMax <= ctx.ScrollbarState.ViewMin || ctx.ScrollbarState.ViewMax > ctx.ScrollbarState.ContentMax || double.IsNaN(ctx.ScrollbarState.ViewMax)) + { + ctx.ScrollbarState.ViewMin = sequence.GetFrameMin(); + double initialSpan = viewWidthPixels / ctx.framePixelWidth; + if (initialSpan <= 0 || double.IsNaN(initialSpan) || double.IsInfinity(initialSpan)) + initialSpan = 100; + ctx.ScrollbarState.ViewMax = ctx.ScrollbarState.ViewMin + initialSpan; + } + + ctx.ScrollbarState.ViewMin = Math.Clamp(ctx.ScrollbarState.ViewMin, ctx.ScrollbarState.ContentMin, ctx.ScrollbarState.ContentMax - ctx.ScrollbarState.MinViewSpan); + ctx.ScrollbarState.ViewMax = Math.Clamp(ctx.ScrollbarState.ViewMax, ctx.ScrollbarState.ViewMin + ctx.ScrollbarState.MinViewSpan, ctx.ScrollbarState.ContentMax); + + firstFrame = (int)Math.Round(ctx.ScrollbarState.ViewMin); + double viewSpan = ctx.ScrollbarState.ViewMax - ctx.ScrollbarState.ViewMin; + if (viewSpan <= 0) viewSpan = 1; + ctx.framePixelWidthTarget = Math.Clamp(ctx.framePixelWidthTarget, 0.1f, 50f); + + ctx.framePixelWidth = Extensions.ImLerp(ctx.framePixelWidth, ctx.framePixelWidthTarget, 0.33f); + + frameCount = sequence.GetFrameMax() - sequence.GetFrameMin(); + if (visibleFrameCount >= frameCount) + firstFrame = sequence.GetFrameMin(); + + // -- + if (!expanded) + { + if(ImGui.InvisibleButton("canvas", new Vector2(canvas_size.X - canvas_pos.X, (float)ctx.ItemHeight))) + { + expanded = true; + }; + draw_list.AddRectFilled(canvas_pos, new Vector2(canvas_size.X + canvas_pos.X, canvas_pos.Y + ctx.ItemHeight), 0xFF3D3837, ImDrawFlags.None); + string tmps = string.Format(sequence.GetCollapseFmt(frameCount, sequenceCount)); + draw_list.AddText(new Vector2(canvas_pos.X + 26, canvas_pos.Y + 2), ctx.Style.Text, tmps); + } + else + { + bool hasScrollBar = true; + Vector2 headerSize = new Vector2(canvas_size.X, (float)ctx.ItemHeight); + Vector2 scrollBarSize = new Vector2(canvas_size.X, 14f); + ImGui.InvisibleButton("topBar", headerSize); + draw_list.AddRectFilled(canvas_pos, Extensions.Add(canvas_pos, headerSize), 0xFFFF0000, ImDrawFlags.None); + Vector2 childFramePos = ImGui.GetCursorScreenPos(); + Vector2 childFrameSize = new Vector2(canvas_size.X, canvas_size.Y - 8f - headerSize.Y - (hasScrollBar ? scrollBarSize.Y : 0)); + ImGui.PushStyleColor(ImGuiCol.FrameBg, 0); + ImGui.BeginChildFrame(889, childFrameSize); + sequence.focused = ImGui.IsWindowFocused(); + ImGui.InvisibleButton("contentBar", new Vector2(canvas_size.X, (float)controlHeight)); + Vector2 contentMin = ImGui.GetItemRectMin(); + Vector2 contentMax = ImGui.GetItemRectMax(); + ImRect contentRect = new ImRect(contentMin, contentMax); + float contentHeight = contentMax.Y - contentMin.Y; + + // full background + draw_list.AddRectFilled(canvas_pos, Extensions.Add(canvas_pos, canvas_size), ctx.Style.FrameBg, ImDrawFlags.None); + + // current frame top + ImRect topRect = new ImRect(new Vector2(canvas_pos.X + ctx.legendWidth, canvas_pos.Y), new Vector2(canvas_pos.X + canvas_size.X, canvas_pos.Y + ctx.ItemHeight)); + + if (!ctx.MovingCurrentFrame && !ctx.MovingScrollBar && ctx.movingEntry == -1 && (sequenceOptions & SEQUENCER_OPTIONS.SEQUENCER_CHANGE_FRAME) != 0 && currentFrame >= 0 && topRect.Contains(io.MousePos) && io.MouseDown[0]) + { + ctx.MovingCurrentFrame = true; + } + if (ctx.MovingCurrentFrame) + { + if (frameCount != 0) + { + currentFrame = (int)((io.MousePos.X - topRect.Min.X) / ctx.framePixelWidth) + firstFrameUsed; + if (currentFrame < sequence.GetFrameMin()) + currentFrame = sequence.GetFrameMin(); + if (currentFrame >= sequence.GetFrameMax()) + currentFrame = sequence.GetFrameMax(); + } + if (!io.MouseDown[0]) + ctx.MovingCurrentFrame = false; + } + + //header + draw_list.AddRectFilled(canvas_pos, new Vector2(canvas_size.X + canvas_pos.X, canvas_pos.Y + ctx.ItemHeight), ctx.Style.MenuBarBg - 0xAAAAAAAA, ImDrawFlags.None); + if ((sequenceOptions & SEQUENCER_OPTIONS.SEQUENCER_ADD) != 0) + { + if (SequencerAddDelButton(draw_list, new Vector2(canvas_pos.X + ctx.legendWidth - ctx.ItemHeight, canvas_pos.Y + 2), true)) + ImGui.OpenPopup("addEntry"); + + if (ImGui.BeginPopup("addEntry")) + { + for (int i = 0; i < sequence.GetItemTypeCount(); i++) + if (ImGui.Selectable(sequence.GetItemTypeName(i))) + { + sequence.Add(i); + selectedEntry = sequence.GetItemCount() - 1; + } + + ImGui.EndPopup(); + popupOpened = true; + } + } + + //header frame number and lines + int modFrameCount = 10; + int frameStep = 1; + while ((modFrameCount * ctx.framePixelWidth) < 150) + { + modFrameCount *= 2; + frameStep *= 2; + } + int halfModFrameCount = modFrameCount / 2; + + void DrawLine(int i, int regionHeight) + { + bool baseIndex = ((i % modFrameCount) == 0) || (i == sequence.GetFrameMax() || i == sequence.GetFrameMin()); + bool halfIndex = (i % halfModFrameCount) == 0; + int px = (int)canvas_pos.X + (int)(i * ctx.framePixelWidth) + ctx.legendWidth - (int)(firstFrameUsed * ctx.framePixelWidth); + int tiretStart = baseIndex ? 4 : (halfIndex ? 10 : 14); + int tiretEnd = baseIndex ? regionHeight : ctx.ItemHeight; + + if (px <= (canvas_size.X + canvas_pos.X) && px >= (canvas_pos.X + ctx.legendWidth)) + { + draw_list.AddLine(new Vector2(px, canvas_pos.Y + tiretStart), new Vector2(px, canvas_pos.Y + tiretEnd - 1), 0xFF606060, 1); + draw_list.AddLine(new Vector2(px, canvas_pos.Y + ctx.ItemHeight), new Vector2(px, canvas_pos.Y + regionHeight - 1), 0x30606060, 1); + } + + if (baseIndex && px > (canvas_pos.X + ctx.legendWidth)) + { + string tmps = string.Format("{0}", i); + draw_list.AddText(new Vector2(px + 3f, canvas_pos.Y), ctx.Style.Text, tmps); + } + } + + void DrawLineContent(int i, int regionHeight) + { + int px = (int)canvas_pos.X + (int)(i * ctx.framePixelWidth) + ctx.legendWidth - (int)(firstFrameUsed * ctx.framePixelWidth); + int tiretStart = (int)contentMin.Y; + int tiretEnd = (int)contentMax.Y; + + if (px <= (canvas_size.X + canvas_pos.X) && px >= (canvas_pos.X + ctx.legendWidth)) + { + draw_list.AddLine(new Vector2(px, tiretStart), new Vector2(px, tiretEnd), 0x30606060, 1); + } + } + + for (int i = sequence.GetFrameMin(); i <= sequence.GetFrameMax(); i += frameStep) + { + DrawLine(i, ctx.ItemHeight); + } + DrawLine(sequence.GetFrameMin(), ctx.ItemHeight); + DrawLine(sequence.GetFrameMax(), ctx.ItemHeight); + + //ImGui.PushClipRect(childFramePos, Extensions.Add(childFramePos, childFrameSize), true); + draw_list.PushClipRect(childFramePos, Extensions.Add(childFramePos, childFrameSize), true); + + // draw item names in the legend rect on the left + int customHeight = 0; + for (int i = 0; i < sequenceCount; i++) + { + int* start, end; + uint color; + sequence.Get(i, &start, &end, null, &color); + Vector2 tpos = new Vector2(contentMin.X + 3, contentMin.Y + i * ctx.ItemHeight + 2 + customHeight); + draw_list.AddText(tpos, 0xFFFFFFFF, sequence.GetItemLabel(i)); + + if ((sequenceOptions & SEQUENCER_OPTIONS.SEQUENCER_DEL) != 0) + { + if (SequencerAddDelButton(draw_list, new Vector2(contentMin.X + ctx.legendWidth - ctx.ItemHeight + 2 - 10, tpos.Y + 2), false)) + ctx.delEntry = i; + + if (SequencerAddDelButton(draw_list, new Vector2(contentMin.X + ctx.legendWidth - ctx.ItemHeight - ctx.ItemHeight + 2 - 10, tpos.Y + 2), true)) + ctx.dupEntry = i; + } + customHeight += (int)sequence.GetCustomHeight(i); + } + + // slots background + customHeight = 0; + for (int i = 0; i < sequenceCount; i++) + { + uint col = (i & 1) != 0 ? 0xFF3A3636u : 0xFF413D3Du; + + int localCustomHeight = (int)sequence.GetCustomHeight(i); + Vector2 pos = new Vector2(contentMin.X + ctx.legendWidth, contentMin.Y + ctx.ItemHeight * i + 1 + customHeight); + Vector2 sz = new Vector2(canvas_size.X + canvas_pos.X, pos.Y + ctx.ItemHeight - 1 + localCustomHeight); + if (!popupOpened && cy >= pos.Y && cy < pos.Y + (ctx.ItemHeight + localCustomHeight) && ctx.movingEntry == -1 && cx > contentMin.X && cx < contentMin.X + canvas_size.X) + { + col += 0x80201008u; + pos.X -= ctx.legendWidth; + } + draw_list.AddRectFilled(pos, sz, col, ImDrawFlags.None); + customHeight += localCustomHeight; + } + draw_list.PushClipRect(Extensions.Add(childFramePos, new Vector2(ctx.legendWidth, 0f)), Extensions.Add(childFramePos, childFrameSize), false); + //ImGui.PushClipRect(Extensions.Add(childFramePos, new Vector2(ctx.legendWidth, 0f)), Extensions.Add(childFramePos, childFrameSize), false); + + // vertical frame lines in content area + for (int i = sequence.GetFrameMin(); i <= sequence.GetFrameMax(); i += frameStep) + { + DrawLineContent(i, (int)contentHeight); + } + DrawLineContent(sequence.GetFrameMin(), (int)contentHeight); + DrawLineContent(sequence.GetFrameMax(), (int)contentHeight); + + // selection + bool selected = selectedEntry >= 0; + if (selected) + { + customHeight = 0; + for (int i = 0; i < selectedEntry; i++) + customHeight += (int)sequence.GetCustomHeight(i); + draw_list.AddRectFilled(new Vector2(contentMin.X, contentMin.Y + ctx.ItemHeight * selectedEntry + customHeight), new Vector2(contentMin.X + canvas_size.X, contentMin.Y + ctx.ItemHeight * (selectedEntry + 1) + customHeight), 0x801080FF, 1f); + } + + // slots + customHeight = 0; + for (int i = 0; i < sequenceCount; i++) + { + int* start, end; + uint color; + sequence.Get(i, &start, &end, null, &color); + int localCustomHeight = (int)sequence.GetCustomHeight(i); + + Vector2 pos = new Vector2(contentMin.X + ctx.legendWidth - firstFrameUsed * ctx.framePixelWidth, contentMin.Y + ctx.ItemHeight * i + 1 + customHeight); + Vector2 slotP1 = new Vector2(pos.X + *start * ctx.framePixelWidth, pos.Y + 2); + Vector2 slotP2 = new Vector2(pos.X + *end * ctx.framePixelWidth + ctx.framePixelWidth, pos.Y + ctx.ItemHeight - 2); + Vector2 slotP3 = new Vector2(pos.X + *end * ctx.framePixelWidth + ctx.framePixelWidth, pos.Y + ctx.ItemHeight - 2 + localCustomHeight); + uint slotColor = color | 0xFF000000u; + uint slotColorHalf = (color & 0xFFFFFFu) | 0x40000000u; + + if (slotP1.X <= (canvas_size.X + contentMin.X) && slotP2.X >= (contentMin.X + ctx.legendWidth)) + { + draw_list.AddRectFilled(slotP1, slotP3, slotColorHalf, 2); + draw_list.AddRectFilled(slotP1, slotP2, slotColor, 2); + } + + ImRect selectRect = new ImRect(slotP1, slotP2); + if (selectRect.Contains(io.MousePos) && io.MouseDoubleClicked[0]) + { + sequence.DoubleClick(i); + } + // Ensure grabbable handles + float max_handle_width = (slotP2.X - slotP1.X) / 3.0f; + float min_handle_width = Math.Min(10.0f, max_handle_width); + float handle_width = Math.Clamp(ctx.framePixelWidth / 2.0f, min_handle_width, max_handle_width); + ImRect[] rects = { + new ImRect(slotP1, new Vector2(slotP1.X + handle_width, slotP2.Y)), + new ImRect(new Vector2(slotP2.X - handle_width, slotP1.Y), slotP2), + new ImRect(slotP1, slotP2) + }; + + uint[] quadColor = { 0xFFFFFFFFu, 0xFFFFFFFFu, slotColor + (selected ? 0u : 0x202020u) }; + if (ctx.movingEntry == -1 && (sequenceOptions & SEQUENCER_OPTIONS.SEQUENCER_EDIT_STARTEND) != 0) + { + for (int j = 2; j >= 0; j--) + { + ImRect rc = rects[j]; + if (!rc.Contains(io.MousePos)) + continue; + draw_list.AddRectFilled(rc.Min, rc.Max, quadColor[j], 2); + } + + for (int j = 0; j < 3; j++) + { + ImRect rc = rects[j]; + if (!rc.Contains(io.MousePos)) + continue; + ImRect subselectRect = new ImRect(childFramePos, + Extensions.Add(childFramePos, childFrameSize)); + if (!subselectRect.Contains(io.MousePos)) + continue; + if (ImGui.IsMouseClicked(0) && !ctx.MovingScrollBar && !ctx.MovingCurrentFrame) + { + ctx.movingEntry = i; + ctx.movingPos = cx; + ctx.movingPart = j + 1; + sequence.BeginEdit(ctx.movingEntry); + break; + } + } + } + + // custom draw + if (localCustomHeight > 0) + { + Vector2 rp = new Vector2(canvas_pos.X + *start, contentMin.Y + ctx.ItemHeight * i + 1 + customHeight); + ImRect customRect = new ImRect(Extensions.Add(rp, new Vector2( ctx.legendWidth - (firstFrameUsed - sequence.GetFrameMin() - 0.5f) * ctx.framePixelWidth, (float)ctx.ItemHeight)), + Extensions.Add(rp, new Vector2(ctx.legendWidth + (sequence.GetFrameMax() - firstFrameUsed - 0.5f + 2f) * ctx.framePixelWidth, (float)(localCustomHeight + ctx.ItemHeight)))); + ImRect clippingRect = new ImRect(Extensions.Add(rp, new Vector2((float)ctx.legendWidth, (float)ctx.ItemHeight)), Extensions.Add(rp, new Vector2(canvas_size.X, (float)(localCustomHeight + ctx.ItemHeight)))); + + ImRect legendRect = new ImRect(Extensions.Add(rp, new Vector2(0f, (float)ctx.ItemHeight)), Extensions.Add(rp, new Vector2((float)ctx.legendWidth, (float)localCustomHeight))); + ImRect legendClippingRect = new ImRect(Extensions.Add(canvas_pos, new Vector2(0f, (float)ctx.ItemHeight)), Extensions.Add(canvas_pos, new Vector2((float)ctx.legendWidth, (float)(localCustomHeight + ctx.ItemHeight)))); + customDraws.Add(new SequencerCustomDraw { Index = i, CustomRect = customRect, LegendRect = legendRect, ClippingRect = clippingRect, LegendClippingRect = legendClippingRect }); + } + else + { + Vector2 rp = new Vector2(canvas_pos.X, contentMin.Y + ctx.ItemHeight * i + customHeight); + ImRect customRect = new ImRect(Extensions.Add(rp, new Vector2(ctx.legendWidth - (firstFrameUsed - sequence.GetFrameMin() - 0.5f) * ctx.framePixelWidth, 0f)), + Extensions.Add(rp, new Vector2(ctx.legendWidth + (sequence.GetFrameMax() - firstFrameUsed - 0.5f + 2f) * ctx.framePixelWidth, (float)ctx.ItemHeight))); + ImRect clippingRect = new ImRect(Extensions.Add(rp, new Vector2((float)ctx.legendWidth, 0f)), Extensions.Add(rp, new Vector2(canvas_size.X, (float)ctx.ItemHeight))); + + compactCustomDraws.Add(new SequencerCustomDraw { Index = i, CustomRect = customRect, LegendRect = new ImRect(), ClippingRect = clippingRect, LegendClippingRect = new ImRect() }); + } + customHeight += localCustomHeight; + } + + // moving + if (ctx.movingEntry >= 0) + { + ImGui.SetNextFrameWantCaptureMouse(true); + int diffFrame = (int)((cx - ctx.movingPos) / ctx.framePixelWidth); + if (Math.Abs(diffFrame) > 0) + { + int* start, end; + uint color; + sequence.Get(ctx.movingEntry, &start, &end, null, &color); + selectedEntry = ctx.movingEntry; + ref int l = ref *start; + ref int r = ref *end; + if ((ctx.movingPart & 1) != 0) + l += diffFrame; + if ((ctx.movingPart & 2) != 0) + r += diffFrame; + if (l < 0) + { + if ((ctx.movingPart & 2) != 0) + r -= l; + l = 0; + } + if ((ctx.movingPart & 1) != 0 && l > r) + l = r; + if ((ctx.movingPart & 2) != 0 && r < l) + r = l; + ctx.movingPos += (int)(diffFrame * ctx.framePixelWidth); + } + if (!io.MouseDown[0]) + { + if (diffFrame == 0 && ctx.movingPart != 0) + { + selectedEntry = ctx.movingEntry; + ret = true; + } + ctx.movingEntry = -1; + sequence.EndEdit(); + } + } + draw_list.PopClipRect(); + draw_list.PopClipRect(); + + // cursor + + + draw_list.PushClipRect(Extensions.Add(childFramePos, new Vector2(ctx.legendWidth, -30f)), Extensions.Add(childFramePos, childFrameSize), false); + if (currentFrame >= firstFrame && currentFrame <= sequence.GetFrameMax()) + { + float cursorWidth = 2f; + float cursorOffset = contentMin.X + ctx.legendWidth + (currentFrame - firstFrameUsed) * ctx.framePixelWidth + ctx.framePixelWidth / 2 - cursorWidth * 0.5f; + + draw_list.AddTriangle(new Vector2(cursorOffset+ .5f, canvas_pos.Y + 10), new Vector2(cursorOffset - 5 , canvas_pos.Y), new Vector2(cursorOffset + 5 , canvas_pos.Y),0xFF2A2AFF, 2f ); + draw_list.AddLine(new Vector2(cursorOffset, canvas_pos.Y + 10), new Vector2(cursorOffset, contentMax.Y), 0xFF2A2AFF, cursorWidth); + + draw_list.AddText(new Vector2(cursorOffset + 10, canvas_pos.Y + 2), 0xFFFFFFFF, $"{currentFrame.ToString()}"); + } + draw_list.PopClipRect(); + + + + draw_list.PushClipRect(Extensions.Add(childFramePos, new Vector2(ctx.legendWidth, 0f)), Extensions.Add(childFramePos, childFrameSize), false); + foreach (var customDraw in customDraws) + sequence.CustomDraw(customDraw.Index, draw_list, customDraw.CustomRect, customDraw.LegendRect, customDraw.ClippingRect, customDraw.LegendClippingRect); + foreach (var customDraw in compactCustomDraws) + sequence.CustomDrawCompact(customDraw.Index, draw_list, customDraw.CustomRect, customDraw.ClippingRect); + draw_list.PopClipRect(); + // copy paste + /*if ((sequenceOptions & (int)SEQUENCER_OPTIONS.SEQUENCER_COPYPASTE) != 0) + { + ImRect rectCopy = new ImRect(new Vector2(contentMin.X + 100, canvas_pos.Y + 2), new Vector2(contentMin.X + 100 + 30, canvas_pos.Y + ctx.ItemHeight - 2)); + bool inRectCopy = rectCopy.Contains(io.MousePos); + uint copyColor = inRectCopy ? 0xFF1080FFu : 0xFF000000u; + draw_list.AddText(rectCopy.Min, copyColor, "Copy"); + + ImRect rectPaste = new ImRect(new Vector2(contentMin.X + 140, canvas_pos.Y + 2), new Vector2(contentMin.X + 140 + 30, canvas_pos.Y + ctx.ItemHeight - 2)); + bool inRectPaste = rectPaste.Contains(io.MousePos); + uint pasteColor = inRectPaste ? 0xFF1080FFu : 0xFF000000u; + draw_list.AddText(rectPaste.Min, pasteColor, "Paste"); + + if (inRectCopy && io.MouseReleased[0]) + { + sequence.Copy(); + } + if (inRectPaste && io.MouseReleased[0]) + { + sequence.Paste(); + } + } */ + + ImGui.EndChildFrame(); + ImGui.PopStyleColor(); + + var scrollbarCursorPos = ImGui.GetCursorPos(); + ImGui.SetCursorPosX(scrollbarCursorPos.X + ctx.legendWidth); + ImGui.SetItemAllowOverlap(); + //Scrollbar logic + bool scrollbarChanged = ZoomScrollbar.Draw("sequencer_zoom", ref ctx.ScrollbarState, 14.0f); + if (scrollbarChanged) + { + firstFrame = (int)Math.Round(ctx.ScrollbarState.ViewMin); + viewSpan = ctx.ScrollbarState.ViewMax - ctx.ScrollbarState.ViewMin; + if (viewSpan <= 0) viewSpan = 1; + ctx.framePixelWidth = (float)(viewWidthPixels / viewSpan); + ctx.framePixelWidthTarget = ctx.framePixelWidth; + } + ImGui.SetCursorPos(scrollbarCursorPos + new Vector2(0, 14.0f)); + + + + } + + ImGui.EndGroup(); + + if (regionRect.Contains(io.MousePos)) + { + bool overCustomDraw = false; + foreach (SequencerCustomDraw custom in customDraws) + { + _sequencerCustomDraw = custom; + if (_sequencerCustomDraw.CustomRect.Contains(io.MousePos)) + { + overCustomDraw = true; + } + } + if (overCustomDraw) + { + } + else + { + // Mouse wheel zoom logic comes later + } + } + + if (expanded) + { + bool overExpanded = SequencerAddDelButton(draw_list, + new Vector2(canvas_pos.X + 2, canvas_pos.Y + 2), !expanded); + if (overExpanded && io.MouseReleased[0]) + { + expanded = !expanded; + } + + } + + if (ctx.delEntry != -1) + { + sequence.Del(ctx.delEntry); + if (selectedEntry == ctx.delEntry || selectedEntry >= sequence.GetItemCount()) + selectedEntry = -1; + } + + if (ctx.dupEntry != -1) + { + sequence.Duplicate(ctx.dupEntry); + } + return ret; + } + + + } + + + } + + diff --git a/Brio/UI/Controls/Sequencer/SequencerContext.cs b/Brio/UI/Controls/Sequencer/SequencerContext.cs new file mode 100644 index 00000000..db6b2948 --- /dev/null +++ b/Brio/UI/Controls/Sequencer/SequencerContext.cs @@ -0,0 +1,36 @@ +using System.Numerics; + +namespace ImSequencer; + +public class SeqContext +{ + public struct SequencerContext { + public float framePixelWidth = 10f; + public float framePixelWidthTarget = 10f; + public int legendWidth = 200; + + public int movingEntry = -1; + public int movingPos = -1; + public int movingPart = -1; + public int delEntry = -1; + public int dupEntry = -1; + public int ItemHeight = 20; + + public bool MovingScrollBar = false; + public bool MovingCurrentFrame = false; + + public bool PanningView = false; + public Vector2 PanningViewSource; + public int PanningViewFrame; + public bool SizingRBar = false; + public bool SizingLBar = false; + public SequencerStyle Style = new SequencerStyle(); + public ZoomScrollbar.State ScrollbarState = new(); + public SequencerContext() + { + } + } + + + +} \ No newline at end of file diff --git a/Brio/UI/Controls/Sequencer/SequencerCustomDraw.cs b/Brio/UI/Controls/Sequencer/SequencerCustomDraw.cs new file mode 100644 index 00000000..c55e897b --- /dev/null +++ b/Brio/UI/Controls/Sequencer/SequencerCustomDraw.cs @@ -0,0 +1,31 @@ +using Dalamud.Bindings.ImGui; + +namespace ImSequencer +{ + public struct SequencerCustomDraw + { + + public int Index; + public ImRect CustomRect; + public ImRect LegendRect; + public ImRect ClippingRect; + public ImRect LegendClippingRect; + + + public SequencerCustomDraw( + int index, ImRect customRect, ImRect legendRect, ImRect clippingRect, ImRect legendClippingRect) + { + Index = index; + CustomRect = customRect; + LegendRect = legendRect; + ClippingRect = clippingRect; + LegendClippingRect = legendClippingRect; + } + + + } +} + + + + diff --git a/Brio/UI/Controls/Sequencer/SequencerOptions.cs b/Brio/UI/Controls/Sequencer/SequencerOptions.cs new file mode 100644 index 00000000..2b32bec1 --- /dev/null +++ b/Brio/UI/Controls/Sequencer/SequencerOptions.cs @@ -0,0 +1,12 @@ +namespace ImSequencer; + +public enum SEQUENCER_OPTIONS +{ + SEQUENCER_EDIT_NONE = 0, + SEQUENCER_EDIT_STARTEND = 1 << 1, + SEQUENCER_CHANGE_FRAME = 1 << 3, + SEQUENCER_ADD = 1 << 4, + SEQUENCER_DEL = 1 << 5, + SEQUENCER_COPYPASTE = 1 << 6, + SEQUENCER_EDIT_ALL = SEQUENCER_EDIT_STARTEND | SEQUENCER_CHANGE_FRAME +} diff --git a/Brio/UI/Controls/Sequencer/SequencerStyle.cs b/Brio/UI/Controls/Sequencer/SequencerStyle.cs new file mode 100644 index 00000000..0ac9fd49 --- /dev/null +++ b/Brio/UI/Controls/Sequencer/SequencerStyle.cs @@ -0,0 +1,19 @@ +using Dalamud.Interface.Style; +using Dalamud.Bindings.ImGui; +using Color = Dalamud.Interface.Style.StyleModelV1; + +namespace ImSequencer; + + +public class SequencerStyle +{ + + private static uint DalamudStyle(string type) + { + return ImGui.ColorConvertFloat4ToU32(Color.DalamudStandard.Colors[type]); + } + public uint Text = DalamudStyle("Text"); + public uint FrameBg = DalamudStyle("FrameBg"); + public uint Header = DalamudStyle("Header"); + public uint MenuBarBg = DalamudStyle("MenuBarBg"); +} \ No newline at end of file diff --git a/Brio/UI/Controls/Sequencer/ZoomScrollbar.cs b/Brio/UI/Controls/Sequencer/ZoomScrollbar.cs new file mode 100644 index 00000000..05d856d2 --- /dev/null +++ b/Brio/UI/Controls/Sequencer/ZoomScrollbar.cs @@ -0,0 +1,219 @@ +using Dalamud.Bindings.ImGui; +using System; + +public sealed class ZoomScrollbar +{ + public struct State + { + public double ContentMin; + public double ContentMax; + public double ViewMin; + public double ViewMax; + public double MinViewSpan; + public double PanSpeed; + public double ZoomSpeed; + internal int ActiveId; + internal double GrabOffset; + } + + private const int Handle_None = 0; + private const int Handle_Left = 1; + private const int Handle_Right = 2; + private const int Handle_Center = 3; + + public static bool Draw(string id, ref State s, float height = 22.0f) + { + if (s.ZoomSpeed <= 0) s.ZoomSpeed = 0.15; + if (s.MinViewSpan <= 0) s.MinViewSpan = (s.ContentMax - s.ContentMin) * 0.001; + + if (s.ContentMax <= s.ContentMin) s.ContentMax = s.ContentMin + 1.0; + s.ViewMin = Math.Clamp(s.ViewMin, s.ContentMin, s.ContentMax - s.MinViewSpan); + s.ViewMax = Math.Clamp(s.ViewMax, s.ViewMin + s.MinViewSpan, s.ContentMax); + + var io = ImGui.GetIO(); + var style = ImGui.GetStyle(); + var draw = ImGui.GetWindowDrawList(); + + var cursor = ImGui.GetCursorScreenPos(); + var availX = ImGui.GetContentRegionAvail().X; + var size = new System.Numerics.Vector2(availX, height); + + ImGui.PushID(id); + ImGui.InvisibleButton("##zoomscroll", size); + bool hovered = ImGui.IsItemHovered(); + bool active = ImGui.IsItemActive(); + + uint colBg = ImGui.GetColorU32(ImGuiCol.ChildBg); + uint colBar = ImGui.GetColorU32(ImGuiCol.ScrollbarGrab); + uint colFill = ImGui.GetColorU32(ImGuiCol.ScrollbarGrab) * 0x88FFFFFF; + uint colGrip = ImGui.GetColorU32(ImGuiCol.ScrollbarGrabActive); + + var rBgMin = cursor; + var rBgMax = new System.Numerics.Vector2(cursor.X + size.X, cursor.Y + size.Y); + draw.AddRectFilled(rBgMin, rBgMax, colBg, style.FrameRounding); + + double contentSpan = s.ContentMax - s.ContentMin; + double viewSpan = s.ViewMax - s.ViewMin; + + float pxPerUnit = (float)(size.X / contentSpan); + double contentMin = s.ContentMin; + + float xFrom(double content) + { + return (float)((content - contentMin) * pxPerUnit) + cursor.X; + } + + float x0 = xFrom(s.ViewMin); + float x1 = xFrom(s.ViewMax); + + float laneY0 = cursor.Y + 0.25f * size.Y; + float laneY1 = cursor.Y + 0.75f * size.Y; + draw.AddRectFilled(new System.Numerics.Vector2(cursor.X, laneY0), + new System.Numerics.Vector2(cursor.X + size.X, laneY1), + colBar, style.FrameRounding); + + var thumbRounding = style.GrabRounding > 0 ? style.GrabRounding : style.FrameRounding; + var thumbMin = new System.Numerics.Vector2(x0, cursor.Y + 3); + var thumbMax = new System.Numerics.Vector2(x1, cursor.Y + size.Y - 3); + draw.AddRectFilled(thumbMin, thumbMax, colFill, thumbRounding); + + float gripW = MathF.Max(4f, MathF.Min(10f, (x1 - x0) * 0.15f)); // visual only + draw.AddRectFilled(new System.Numerics.Vector2(x0, cursor.Y + 3), + new System.Numerics.Vector2(x0 + gripW, cursor.Y + size.Y - 3), + colGrip, thumbRounding); + draw.AddRectFilled(new System.Numerics.Vector2(x1 - gripW, cursor.Y + 3), + new System.Numerics.Vector2(x1, cursor.Y + size.Y - 3), + colGrip, thumbRounding); + + var mx = io.MousePos.X; + var my = io.MousePos.Y; + bool inThumb = (mx >= x0 && mx <= x1 && my >= thumbMin.Y && my <= thumbMax.Y); + bool inLeftGrip = (mx >= x0 && mx <= x0 + gripW && inThumb); + bool inRightGrip = (mx >= x1 - gripW && mx <= x1 && inThumb); + bool inCenter = (inThumb && !inLeftGrip && !inRightGrip); + + if (hovered && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) + { + if (inLeftGrip) { s.ActiveId = Handle_Left; s.GrabOffset = 0; } + else if (inRightGrip) { s.ActiveId = Handle_Right; s.GrabOffset = 0; } + else if (inCenter) { s.ActiveId = Handle_Center; s.GrabOffset = mx - x0; } + else { s.ActiveId = Handle_Center; s.GrabOffset = (x1 - x0) * 0.5f; } + } + + if (ImGui.IsMouseReleased(ImGuiMouseButton.Left)) + { + if (s.ActiveId != Handle_None) + { + if (s.ViewMax < s.ViewMin + s.MinViewSpan) + { + s.ViewMax = s.ViewMin + s.MinViewSpan; + } + } + s.ActiveId = Handle_None; + } + + bool changed = false; + + if (hovered) + { + float wheel = io.MouseWheel; + if (Math.Abs(wheel) > 0.0001f) + { + double t = Math.Clamp((mx - cursor.X) / size.X, 0.0, 1.0); + ZoomAround(ref s, 1.0 - wheel * s.ZoomSpeed, t); + changed = true; + } + } + + if (s.ActiveId != Handle_None && ImGui.IsMouseDown(ImGuiMouseButton.Left)) + { + double newContentPos = ((mx - cursor.X) / pxPerUnit) + contentMin; + + switch (s.ActiveId) + { + case Handle_Left: + { + double newMin = newContentPos; + newMin = Math.Min(newMin, s.ViewMax - s.MinViewSpan); + newMin = Math.Max(newMin, s.ContentMin); + + s.ViewMin = newMin; + changed = true; + break; + } + case Handle_Right: + { + double newMax = newContentPos; + newMax = Math.Max(newMax, s.ViewMin + s.MinViewSpan); + newMax = Math.Min(newMax, s.ContentMax); + + s.ViewMax = newMax; + changed = true; + break; + } + case Handle_Center: + { + double span = s.ViewMax - s.ViewMin; + + double grabOffsetContent = s.GrabOffset / pxPerUnit; + double newMin = newContentPos - grabOffsetContent; + double newMax = newMin + span; + + if (newMin < s.ContentMin) + { + newMin = s.ContentMin; + newMax = newMin + span; + } + if (newMax > s.ContentMax) + { + newMax = s.ContentMax; + newMin = newMax - span; + } + + s.ViewMin = newMin; + s.ViewMax = newMax; + changed = true; + break; + } + } + } + + if (hovered && ImGui.IsKeyDown(ImGuiKey.LeftCtrl)) + { + float wheel = io.MouseWheel; + if (Math.Abs(wheel) > 0.0001f) + { + double t = Math.Clamp((mx - cursor.X) / size.X, 0.0, 1.0); + ZoomAround(ref s, 1.0 - wheel * (s.ZoomSpeed * 1.75), t); + changed = true; + } + } + + ImGui.PopID(); + return changed; + } + + private static void ZoomAround(ref State s, double zoomFactor, double pivot01) + { + double span = s.ViewMax - s.ViewMin; + double center = s.ViewMin + span * pivot01; + + double newSpan = Math.Max(s.MinViewSpan, Math.Min((s.ContentMax - s.ContentMin), span * zoomFactor)); + double newMin = center - newSpan * pivot01; + double newMax = newMin + newSpan; + + if (newMin < s.ContentMin) + { + newMin = s.ContentMin; + newMax = newMin + newSpan; + } + if (newMax > s.ContentMax) + { + newMax = s.ContentMax; + newMin = newMax - newSpan; + } + + s.ViewMin = newMin; + s.ViewMax = newMax; + } +} From 3f0d8ea12f504dfc3b3e16c61a95dd1607225d70 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Thu, 27 Nov 2025 18:00:59 -0600 Subject: [PATCH 2/2] Timeline beta from Glorou/Karou (#198) * Initial Sequencer setup * Small update --------- Co-authored-by: Karou <_> Co-authored-by: Glorou <93338547+Glorou@users.noreply.github.com> --- Brio/Brio.cs | 2 + Brio/Brio.csproj | 6 + Brio/Game/Timeline/TimelineManager.cs | 9 + Brio/UI/Controls/Editors/TimelineEditor.cs | 361 +++++++++++++++++++++ Brio/UI/UIManager.cs | 12 +- Brio/UI/Windows/MainWindow.cs | 32 +- 6 files changed, 410 insertions(+), 12 deletions(-) create mode 100644 Brio/Game/Timeline/TimelineManager.cs create mode 100644 Brio/UI/Controls/Editors/TimelineEditor.cs diff --git a/Brio/Brio.cs b/Brio/Brio.cs index 5f52265f..c00ec3e5 100644 --- a/Brio/Brio.cs +++ b/Brio/Brio.cs @@ -29,6 +29,7 @@ using Microsoft.Extensions.DependencyInjection; using System; using System.Diagnostics; +using Brio.UI.Controls.Editors; namespace Brio; @@ -204,6 +205,7 @@ private static ServiceCollection SetupServices(DalamudServices dalamudServices) serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); // serviceCollection.AddSingleton(); return serviceCollection; diff --git a/Brio/Brio.csproj b/Brio/Brio.csproj index 34d8df6a..69af53aa 100644 --- a/Brio/Brio.csproj +++ b/Brio/Brio.csproj @@ -48,4 +48,10 @@ + + + ..\..\MockSequencer\ImSequencer\ImSequencer\bin\Debug\net9.0-windows\ImSequencer.dll + + + diff --git a/Brio/Game/Timeline/TimelineManager.cs b/Brio/Game/Timeline/TimelineManager.cs new file mode 100644 index 00000000..04c09e68 --- /dev/null +++ b/Brio/Game/Timeline/TimelineManager.cs @@ -0,0 +1,9 @@ +using System.Numerics; + +namespace Brio.Game.Timeline; + +public class TimelineManager +{ + public record CameraKeyframe(int Frame, Vector3 Position, Quaternion Rotation, float FoV); + +} diff --git a/Brio/UI/Controls/Editors/TimelineEditor.cs b/Brio/UI/Controls/Editors/TimelineEditor.cs new file mode 100644 index 00000000..3a0fab49 --- /dev/null +++ b/Brio/UI/Controls/Editors/TimelineEditor.cs @@ -0,0 +1,361 @@ +using Brio.Capabilities.Actor; +using Brio.Config; +using Brio.Entities; +using Brio.Files; +using Brio.Game.Actor.Extensions; +using Brio.Game.Cutscene; +using Brio.Game.GPose; +using Brio.Game.Posing; +using Brio.Resources; +using Brio.UI.Controls.Selectors; +using Brio.UI.Controls.Stateless; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Numerics; +using Dalamud.Interface.Windowing; +using Dalamud.Plugin; +using static Brio.Game.Actor.ActionTimelineService; +using ImSequencer; +using ImSequencer.ImCurveEdit; +using ImSequencer.Memory; + +namespace Brio.UI.Controls.Editors; + + +public unsafe class RampEdit : ImSequencer.ImCurveEdit.CurveContext +{ + + public RampEdit() + { + var temp = new Random(); + + mPts = new Vector2[3][]; + + for (var i = 0; i < 3; i++) + { + mPts[i] = new Vector2[3]; + for (var j = 0; j < 3; j++) + { + + mPts[i][j] = new Vector2(temp.NextSingle() % 1, temp.NextSingle() % 1); + } + SortEntries(i); + } + + + } + + private Vector2[][] mPts; + + public override int GetCurveCount() + { + return mPts.Length; + } + + public override int GetPointCount(int curveIndex) + { + return mPts[curveIndex].Length; + } + + public override uint GetCurveColor(int curveIndex) + { + switch (curveIndex) + { + case 0: + return 0xFF0000FF; + case 1: + return 0xFF00AA00; + case 2: + return 0xFFFF0000; + default: + return 0xFF0AAA00; + } + + } + + public override Vector2[] GetPoints(int curveIndex) + { + return mPts[curveIndex]; + } + + public override int EditPoint(int curveIndex, int pointIndex, Vector2 value) + { + mPts[curveIndex][pointIndex] = NormalizeVector(value); + SortEntries(curveIndex); + return 1; + } + + public override void AddPoint(int curveIndex, Vector2 value) + { + Vector2[] tempPts = new Vector2[mPts[curveIndex].Length + 1]; + mPts[curveIndex].CopyTo(tempPts, 0); + tempPts[^1] = value; + mPts[curveIndex] = tempPts; + + } + + private void SortEntries(int curveIndex) + { + mPts[curveIndex] = [..mPts[curveIndex].OrderBy(x =>x.X)]; + } + + private Vector2 NormalizeVector(Vector2 vec) + { + float x = float.Max(float.Min(vec.X, 1), 0); + float y = float.Max(float.Min(vec.Y, 1), 0); + return new Vector2(x, y); + } +} + + +public unsafe class Sequencer : Window, SequenceInterface +{ + + public struct Item + { + public uint color; + public int start; + public int end; + public ItemType type; //switch this to enum later + public bool expanded; + public unsafe object* reference; + + public unsafe Item(ItemType _type, int _start, int _end, bool _expanded, object* _reference = null) + { + start = _start; + end = _end; + type = _type; + expanded = _expanded; + reference = _reference; + } + //public Item(int _type, int _start, int _end, bool _expanded) => Item((ItemType)_type, _start, _end, expanded); + } + public enum ItemType + { + Camera, + Light, + Bone + } + + private int frameMin = 0; + private int frameMax = 100; + public UnsafeList items = []; + + // We give this window a hidden ID using ## + // So that the user will see "My Amazing Window" as window title, + // but for ImGui the ID is "My Amazing Window##With a hidden ID" + public Sequencer(IDalamudPluginInterface pluginInterfacePlugin ) + : base("Timeline##With a hidden ID", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse) + { + SizeConstraints = new WindowSizeConstraints + { + MinimumSize = new Vector2(375, 330), + MaximumSize = new Vector2(float.MaxValue, float.MaxValue) + }; + items.Add(new Item + { + color = 0xFF0000, + end = 20, + start = 0, + type = 0, + expanded = false + }); + rampEdit = new Dictionary(); + for (var i = 0; i < 4; i++) + { + var temp = new RampEdit(); + rampEdit.Add(i, temp); + } + } + + public void Dispose() { } + private int _index; + private bool _expanded = true; + private int _currFrame; + private int _firstFrame; + public Dictionary rampEdit; + + public override void Draw() + { + var ptr = ImGui.GetWindowDrawList(); + ImRect CustomRect = new ImRect(new Vector2(ImGui.GetWindowWidth(), ImGui.GetWindowHeight())); + ImRect LegendRect = new ImRect(new Vector2(ImGui.GetWindowWidth() * .3f, ImGui.GetWindowHeight())); + ImRect LegendClip = new ImRect(new Vector2(ImGui.GetWindowWidth() * .3f, ImGui.GetWindowHeight())); + ImRect CustomClip = new ImRect(new Vector2(ImGui.GetWindowWidth(), ImGui.GetWindowHeight())); + + ImGui.PushItemWidth(75f); + ImGui.InputInt("Frame Min", ref frameMin); + ImGui.SameLine(); + ImGui.InputInt("Frame Max", ref frameMax); + ImGui.Separator(); + + ImGui.PopItemWidth(); + + + ImSequencer.ImSequencer.Sequencer(this, ref _currFrame , ref _expanded, ref _index, ref _firstFrame, options); + } + public bool focused { get; set; } + public int GetFrameMin() => frameMin; + public SEQUENCER_OPTIONS options = SEQUENCER_OPTIONS.SEQUENCER_ADD | SEQUENCER_OPTIONS.SEQUENCER_CHANGE_FRAME | + SEQUENCER_OPTIONS.SEQUENCER_COPYPASTE | SEQUENCER_OPTIONS.SEQUENCER_EDIT_ALL; + + + public int GetFrameMax() => frameMax; + + public int GetItemCount() => items.Size; + + public void BeginEdit(int index) + { + } + + public void EndEdit() + { + } + + public int GetItemTypeCount() => Enum.GetNames(typeof(ItemType)).Length; + + public string GetItemTypeName(int index) => items[index].type.ToString(); + + public string GetItemLabel(int index) + { + return $"[{index.ToString()}] {Enum.GetName(items[index].type)}"; + } + + public string GetCollapseFmt(int frameCount, int sequenceCount) + { + return $"{frameCount.ToString()} frames / {sequenceCount.ToString()} entries"; + } + + public void Get(int index, int** start, int** end, int* type, uint* color) + { + if (start != null) *start = &items.Data[index].start; + if (end != null) *end = &items.Data[index].end; + if (type != null) *type = (int)items[index].type; + if (color != null) *color = 0xFFAA8080; + } + + public void Add(int index) + { + items.Add(new Item() + { + color = 0000000, + end = 60, + expanded = true, + start = 40, + type = 0 + }); + } + + public void Del(int index) + { + } + + public void Duplicate(int index) + { + } + + public void Copy() + { + } + + public void Paste() + { + } + + public uint GetCustomHeight(int index) + { + return (uint)(items[index].expanded ? 300 : 0); + } + + public void DoubleClick(int index) + { + + var item = items.GetPointer(index); + if (item->expanded) + { + item->expanded = false; + return; + } + + for (var i = 0; i < items.Count; i++) + { + var at = items.GetPointer(i); + at->expanded = false; + } + item->expanded = true; + + } + + public unsafe void CustomDraw( + int index, ImDrawListPtr drawList, ImRect customRect, ImRect legendRect, ImRect clippingRect, + ImRect legendClippingRect) + { + if (!rampEdit.TryGetValue(index, out var ramp)) + { + return; + }; + var labels = new[] { "Translation", "Rotation" , "Scale"}; + ramp.Max = new Vector2((float)items[index].end/frameMax, 1f); + ramp.Min = new Vector2((float)(items[index].start-frameMin)/frameMax, 0f); + drawList.PushClipRect(clippingRect.Min, clippingRect.Max, true); + for (int i = 0; i < 3; i++) + { + Vector2 pta = new Vector2(legendRect.Min.X + 30, legendRect.Min.Y + i * 14f); + Vector2 ptb = new Vector2(legendRect.Max.X, legendRect.Min.Y + (i + 1) * 14f); + drawList.AddText(pta, 0xFFFFFFFF, labels[i]); + var imRect = new ImRect(pta, ptb); + if (imRect.Contains(ImGui.GetMousePos()) && ImGui.IsMouseClicked(0)) + { + //rampEdit + } + + } + ImGui.SetCursorScreenPos(customRect.Min); + //drawList.AddRect(customRect.Min, customRect.Max, 0xFFAABBCC); + ramp.Range = new Vector2(customRect.Min.X, customRect.Max.X); + ImCurveEdit.Edit(ramp, customRect.Max-customRect.Min,(uint)(137+ index) ); + drawList.PopClipRect(); + drawList.PushClipRect(clippingRect.Min, clippingRect.Max, true); + rampEdit[index] = ramp; + + + drawList.PopClipRect(); + + } + + public void CustomDrawCompact(int index, ImDrawListPtr drawList, ImRect customRect, ImRect clippingRect) + { + + if (!rampEdit.TryGetValue(index, out var ramp)) + { + return; + }; + drawList.PushClipRect(clippingRect.Min, clippingRect.Max, true); + for (int i = 0; i < 3; i++) + { + for (int j = 0; j < ramp.GetPointCount(i); j++) + { + float p = ramp.GetPoints(i)[j].X; + if (p < items[index].start || p > items[index].end) + continue; + float r = (p*(items[index].end - items[index].start) + frameMin) / (float)(frameMax - frameMin); + float x = Extensions.ImLerp(customRect.Min.X, customRect.Max.X, r); + drawList.AddLine(new Vector2(x, customRect.Min.Y + 6), new Vector2(x, customRect.Max.Y - 4), 0xAA000000, + 4.0f); + + } + } + rampEdit[index] = ramp; + drawList.PopClipRect(); + } + + private static bool IsOpen = false; + +} diff --git a/Brio/UI/UIManager.cs b/Brio/UI/UIManager.cs index 060a620b..76e86d22 100644 --- a/Brio/UI/UIManager.cs +++ b/Brio/UI/UIManager.cs @@ -14,6 +14,7 @@ using FFXIVClientStructs.FFXIV.Common.Lua; using System; using System.Collections.Generic; +using Brio.UI.Controls.Editors; namespace Brio.UI; @@ -39,6 +40,7 @@ public class UIManager : IDisposable private readonly PosingGraphicalWindow _graphicalWindow; private readonly CameraWindow _cameraWindow; private readonly LightWindow _lightWindow; + private readonly Sequencer _sequencer; private readonly ITextureProvider _textureProvider; private readonly IToastGui _toastGui; @@ -90,6 +92,7 @@ public UIManager AutoSaveWindow autoSaveWindow, MCDFWindow mCDFWindow, LightWindow lightWindow, + Sequencer sequencer, PenumbraService penumbraService, GlamourerService glamourerService @@ -119,7 +122,8 @@ GlamourerService glamourerService _autoSaveWindow = autoSaveWindow; _mCDFWindow = mCDFWindow; _lightWindow = lightWindow; - + _sequencer = sequencer; + _framework = framework; _penumbraService = penumbraService; @@ -143,6 +147,7 @@ GlamourerService glamourerService _windowSystem.AddWindow(_autoSaveWindow); _windowSystem.AddWindow(_mCDFWindow); _windowSystem.AddWindow(_lightWindow); + _windowSystem.AddWindow(_sequencer); _gPoseService.OnGPoseStateChange += OnGPoseStateChange; _configurationService.OnConfigurationChanged += ApplySettings; @@ -184,6 +189,11 @@ public void ShowMainWindow() _mainWindow.IsOpen = true; } + public void ShowSequencer() + { + _sequencer.IsOpen = true; + } + public void NotifyError(string message) { _toastGui.ShowError(message); diff --git a/Brio/UI/Windows/MainWindow.cs b/Brio/UI/Windows/MainWindow.cs index 9227af57..a3418ff6 100644 --- a/Brio/UI/Windows/MainWindow.cs +++ b/Brio/UI/Windows/MainWindow.cs @@ -16,6 +16,7 @@ using Dalamud.Interface.Windowing; using System; using System.Numerics; +using Brio.UI.Controls.Editors; namespace Brio.UI.Windows; @@ -33,7 +34,8 @@ public class MainWindow : Window, IDisposable private readonly AutoSaveService _autoSaveService; private readonly HistoryService _groupedUndoService; private readonly MCDFService _mCDFService; - + private readonly Sequencer _sequencer; + public MainWindow( ConfigurationService configService, SettingsWindow settingsWindow, @@ -45,7 +47,8 @@ public MainWindow( GPoseService gPoseService, ProjectWindow projectWindow, AutoSaveService autoSaveService, - MCDFService mCDFService + MCDFService mCDFService, + Sequencer sequencer ) : base($" {Brio.Name} [{configService.Version}]###brio_main_window", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.AlwaysAutoResize) { @@ -63,6 +66,7 @@ MCDFService mCDFService _projectWindow = projectWindow; _autoSaveService = autoSaveService; _mCDFService = mCDFService; + _sequencer = sequencer; SizeConstraints = new WindowSizeConstraints { @@ -113,7 +117,7 @@ public override void Draw() private void DrawHeaderButtons() { float buttonWidths = 25 * ImGuiHelpers.GlobalScale; - float line1FinalWidth = ImBrio.GetRemainingWidth() - ((buttonWidths * 2) + (ImGui.GetStyle().ItemSpacing.X * 2) + ImGui.GetStyle().WindowBorderSize); + float line1FinalWidth = ImBrio.GetRemainingWidth() - ((buttonWidths * 1) + (ImGui.GetStyle().ItemSpacing.X * 2) + ImGui.GetStyle().WindowBorderSize); float line1Width = (line1FinalWidth / 2) - 3; @@ -134,14 +138,6 @@ private void DrawHeaderButtons() if(ImBrio.Button("Library", FontAwesomeIcon.Book, new Vector2(line1Width, 0), centerTest: true)) _libraryWindow.Toggle(); } - - ImGui.SameLine(); - if(ImBrio.FontIconButton(FontAwesomeIcon.InfoCircle, new(buttonWidths, 0))) - _infoWindow.Toggle(); - - if(ImGui.IsItemHovered()) - ImGui.SetTooltip("Information & Changelog"); - ImGui.SameLine(); if(ImBrio.FontIconButton(FontAwesomeIcon.Cog, new(buttonWidths, 0))) _settingsWindow.Toggle(); @@ -149,7 +145,21 @@ private void DrawHeaderButtons() if(ImGui.IsItemHovered()) ImGui.SetTooltip("Settings"); + using(ImRaii.Disabled(_gPoseService.IsGPosing == false)) + { + if(ImBrio.Button("Timeline", FontAwesomeIcon.CodeCommit, new Vector2(line1Width, 0), centerTest: true)) + _sequencer.Toggle(); + ImGui.SameLine(); + if(ImBrio.Button("Placeholder", FontAwesomeIcon.Egg, new Vector2(line1Width, 0), centerTest: true)) + _libraryWindow.Toggle(); + } // + ImGui.SameLine(); + if(ImBrio.FontIconButton(FontAwesomeIcon.InfoCircle, new(buttonWidths, 0))) + _infoWindow.Toggle(); + + if(ImGui.IsItemHovered()) + ImGui.SetTooltip("Information & Changelog"); using(ImRaii.Disabled(_mCDFService.IsApplyingMCDF)) FileUIHelpers.DrawProjectPopup(_sceneService, _entityManager, _projectWindow, _autoSaveService);