#nullable enable using System.Text; using ImGuiNET; using T3.Core.Animation; using T3.Core.DataTypes.DataSet; using T3.Core.DataTypes.Vector; using T3.Core.Utils; using T3.Editor.Gui.Interaction; using T3.Editor.Gui.Styling; using T3.Editor.Gui.UiHelpers; using T3.Editor.Gui.Windows.TimeLine; using T3.Editor.Gui.Windows.TimeLine.Raster; using T3.Editor.UiModel.Commands; using T3.Editor.UiModel.Commands.Animation; namespace T3.Editor.Gui.OutputUi; /// /// Provides a simple canvas to visualize a . /// internal sealed class DataSetViewCanvas { /// /// Raw-DataSet draw — used by for live IO logging. /// The time axis represents wall-clock , so the /// playhead matches the moment events arrived. /// internal void Draw(DataSet? dataSet) => DrawInternal(dataSet, mapping: null, externalCanvas: null); /// /// Clip-aware draw — used by when the source is a /// timeline-bound clip. The time axis represents the clip's source-time space and the /// playhead is mapped from through the clip's /// , so a paused timeline shows a stationary playhead at /// the file position the engine would sample. /// internal void Draw(DataClip? clip) { if (clip?.Set == null) { DrawInternal(null, mapping: null, externalCanvas: null); return; } DrawInternal(clip.Set, clip.Mapping, externalCanvas: null); } /// /// Embedded draw — used by panes that host this view inside a parent canvas (today: /// ). The parent's /// takes over X transforms so event positions line up /// with the parent's ruler / playhead, and this view skips its own chrome / raster / /// auto-scroll (the parent owns those). The optional /// callback runs inside this view's Scrollable child window — that's the only place /// host-supplied widgets (close button, scroll-reset) can land without being blocked /// by ImGui's child-window hit-rect rules. /// internal void DrawEmbedded(DataClip? clip, ScalableCanvas externalCanvas, Action? drawOverlay = null) { if (clip?.Set == null) { DrawInternal(null, mapping: null, externalCanvas: externalCanvas, drawOverlay: drawOverlay); return; } DrawInternal(clip.Set, clip.Mapping, externalCanvas: externalCanvas, drawOverlay: drawOverlay); } private void DrawInternal(DataSet? dataSet, TimeRangeMapping? mapping, ScalableCanvas? externalCanvas, Action? drawOverlay = null) { if (dataSet == null) return; _mapping = mapping; _externalCanvas = externalCanvas; _embeddedOverlay = drawOverlay; // Very ugly hack to prevent scaling the output above window size var keepScale = T3Ui.UiScaleFactor; T3Ui.UiScaleFactor = 1; DrawCanvas(); T3Ui.UiScaleFactor = keepScale; _externalCanvas = null; _embeddedOverlay = null; return; void DrawCanvas() { // Two timebases share one canvas: // * runtime mode (no mapping) — currentTime is wall-clock seconds, so the // playhead tracks event arrivals. // * clip mode (mapping present) — currentTime is source seconds derived from // the timeline playhead. When the timeline is paused the marker freezes at // the file position the engine would sample; scrubbing moves it along the // recorded source axis instead of wall-clock. var currentTime = _mapping is { } m && Playback.Current is { } pb ? m.LocalBarsToSourceSecs(pb.TimeInBars) : Playback.RunTimeInSecs; ImGui.BeginChild("Scrollable", Vector2.Zero, ImGuiChildFlags.None, ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoBackground); var isRangeSelected = Math.Abs(_selectRangeStart - _selectRangeEnd) > 0.001; var areChannelsSelected = _selectedChannels.Count > 0; // Embedded mode skips the toolbar — the host pane (e.g. InlineDataClipArea) // owns its own chrome. ShowInteraction still wins when the host explicitly // opts out via that flag in standalone usage. if (ShowInteraction && !IsEmbedded) { CustomComponents.ToggleButton(ref _onlyRecentEvents, "Only Recent", Vector2.Zero); ImGui.SameLine(); CustomComponents.ToggleButton(ref _scroll, "Scroll", Vector2.Zero); //ImGui.Checkbox("Filter Recent ", ref _onlyRecentEvents); // ImGui.Checkbox("Scroll ", ref _scroll); // Show stats { ImGui.SameLine(0, 20); ImGui.PushStyleColor(ImGuiCol.Text, areChannelsSelected || isRangeSelected ? UiColors.ForegroundFull.Rgba : UiColors.TextMuted.Rgba); var channelCount = areChannelsSelected ? _selectedChannels.Count : dataSet.Channels.Count; var selectedDuration = Math.Abs(_selectRangeStart - _selectRangeEnd); if (selectedDuration < 0.001) { selectedDuration = _lastEventTime - _firstEventTime; } var rangeLabel = selectedDuration switch { < 0.001 => string.Empty, < 1 => $"{(selectedDuration * 1000):0.0}ms", _ => $"{selectedDuration:0.0}s" }; if (ImGui.Button($"{channelCount} Channels / {_activeEventCount} Events / {rangeLabel}###stats")) { Log.Debug(" clear"); _selectedChannels.Clear(); _selectRangeStart = 0; _selectRangeEnd = 0; } ImGui.SameLine(0, 1); ImGui.PopStyleColor(); if (ImGui.Button("Remove")) { // Three intents map onto two command modes: // selection + range → drop events in range on the selected channels // selection + no range → drop the selected channels themselves // no selection + range → drop events in range across all channels // (no-selection + no-range was previously "clear every channel's events" // — now folded into the channel-removal path so it actually removes the // channels, matching the user mental model of "Remove with nothing // narrowed = drop the lot".) ICommand? command = null; if (isRangeSelected) { var channels = areChannelsSelected ? _selectedChannels.ToList() : (IReadOnlyList)dataSet.Channels; command = RemoveDataSetItemsCommand.ForEventRange(dataSet, channels, _selectRangeStart, _selectRangeEnd); } else { var channels = areChannelsSelected ? _selectedChannels.ToList() : dataSet.Channels.ToList(); command = RemoveDataSetItemsCommand.ForChannels(dataSet, channels); } UndoRedoStack.AddAndExecute(command); _selectedChannels.Clear(); _selectRangeStart = 0; _selectRangeEnd = 0; } ImGui.SameLine(); if (ImGui.Button("Copy as CSV")) { try { var sb = new StringBuilder(); var separator = "\t"; var channelList = areChannelsSelected ? _selectedChannels.ToList() : dataSet.Channels; sb.Append("Time"); sb.Append(separator); double sortedMin; double sortedMax; if (isRangeSelected) { sortedMin = _selectRangeStart; sortedMax = _selectRangeEnd; if (sortedMin > sortedMax) { (sortedMin, sortedMax) = (sortedMax, sortedMin); } } else { sortedMin = _firstEventTime; sortedMax = _lastEventTime; } const float timeResolution = 1 / 60f; var maxTimeSlotCount = (int)((sortedMax - sortedMin) / timeResolution); if (maxTimeSlotCount > 0 && channelList.Count > 0) { var timeSlots = new DataEvent?[maxTimeSlotCount, channelList.Count]; for (var channelIndex = 0; channelIndex < channelList.Count; channelIndex++) { var channel = channelList[channelIndex]; sb.Append(channel.Path.Last()); sb.Append(separator); var minIndex = channel.FindIndexForTime(sortedMin); var maxIndex = channel.FindIndexForTime(sortedMax); for (var i = minIndex; i < maxIndex; i++) { var e = channel.Events[i]; var timeSlotIndex = (int)(((e?.Time ?? 0) - sortedMin) / timeResolution); if (timeSlotIndex >= 0 && timeSlotIndex < maxTimeSlotCount) timeSlots[timeSlotIndex, channelIndex] = e; } } sb.Append("\n"); for (var rowIndex = 0; rowIndex < maxTimeSlotCount; rowIndex++) { var isAnyNonEmpty = false; for (var channelIndex = 0; channelIndex < channelList.Count; channelIndex++) { if (timeSlots[rowIndex, channelIndex] != null) { isAnyNonEmpty = true; break; } } if (!isAnyNonEmpty) continue; sb.Append($"{sortedMin + rowIndex * timeResolution:0.000}"); sb.Append(separator); for (var channelIndex = 0; channelIndex < channelList.Count; channelIndex++) { var e = timeSlots[rowIndex, channelIndex]; if (e == null) { sb.Append(separator); } else { sb.Append(e.Value); } sb.Append(separator); } sb.AppendLine(); } } ImGui.SetClipboardText(sb.ToString()); } catch (Exception e) { Log.Warning("Failed to copy as CSV: " + e.Message); } } } } // Canvas state has to settle BEFORE the raster reads Scale/Scroll, otherwise // the raster anchors to the previous frame's scope while events get drawn // against the freshly-computed scope further down. In runtime mode the two // were close enough to look fine; in clip mode currentTime jumps to a // completely different value range (source seconds via the mapping) and the // raster ends up rendered at coordinates that no longer overlap the visible // viewport — so it disappears. // // Embedded mode: the host's parent canvas owns scope updates / pan / zoom / // raster. We just read the parent's transform via EffectiveCanvas and skip // these standalone-canvas hooks so we don't fight the parent's interaction. if (!IsEmbedded) { _canvas.UpdateCanvas(out _); if (ImGui.IsWindowHovered(ImGuiHoveredFlags.ChildWindows | ImGuiHoveredFlags.RootAndChildWindows) && ImGui.IsMouseClicked(ImGuiMouseButton.Right)) { _scrollPosStart = ImGui.GetScrollY(); _isDraggingCanvas = true; } if (_isDraggingCanvas && !ImGui.IsMouseDown(ImGuiMouseButton.Right)) { _isDraggingCanvas = false; } if (_isDraggingCanvas) { var dragDelta = ImGui.GetMouseDragDelta(ImGuiMouseButton.Right); var newScrollY = _scrollPosStart - dragDelta.Y; ImGui.SetScrollY((int)newScrollY); } if (_scroll) { var windowWidth = ImGui.GetWindowWidth(); var width = _canvas.InverseTransformDirection(ImGui.GetWindowSize()).X; var scale = windowWidth / width; _canvas.SetScopeInstant(new CanvasScope { Scale = new Vector2(scale, -1), Scroll = new Vector2((float)currentTime - (windowWidth - 50 * T3Ui.UiScaleFactor) / scale, 0) }); } _standardRaster.Draw(_canvas); } else { // Embedded: RMB drag pans both axes (Y inside this child, X on the parent // timeline canvas so the timeline scrolls in sync); mouse wheel zooms X // on the parent so events stay aligned with the ruler. var hovered = ImGui.IsWindowHovered(ImGuiHoveredFlags.ChildWindows); if (hovered && ImGui.IsMouseClicked(ImGuiMouseButton.Right)) { _scrollPosStart = ImGui.GetScrollY(); _externalScrollXAtDragStart = _externalCanvas!.Scroll.X; _isDraggingCanvas = true; } if (_isDraggingCanvas && !ImGui.IsMouseDown(ImGuiMouseButton.Right)) _isDraggingCanvas = false; if (_isDraggingCanvas) { var dragDelta = ImGui.GetMouseDragDelta(ImGuiMouseButton.Right); ImGui.SetScrollY((int)(_scrollPosStart - dragDelta.Y)); if (_externalCanvas is TimeLineCanvas tlc) tlc.PanXFromEmbedded(_externalScrollXAtDragStart, dragDelta.X); } if (hovered && !_isDraggingCanvas) { var wheel = ImGui.GetIO().MouseWheel; if (Math.Abs(wheel) > 0.0001f && _externalCanvas is TimeLineCanvas tlcZoom) tlcZoom.ZoomXFromEmbedded(wheel, ImGui.GetMousePos()); } } var dl = ImGui.GetWindowDrawList(); var min = ImGui.GetWindowPos(); var max = ImGui.GetContentRegionAvail() + min; var visibleMinTime = ScreenXToEventSecs(min.X); var visibleMaxTime = ScreenXToEventSecs(max.X); // Clip-mode boundary overlay: shade regions outside the source slice the clip // actually plays and draw thin lines at the edges. Mirrors the look the // TimeClip composition view uses for SourceRange (ClipRange.cs) so the two // canvases feel like part of one editor. Drawn here — after the raster but // before events — so the (low-alpha) overlay sits behind the per-channel // markers and the current-time marker. if (_mapping is { } mapping) { var bpm = mapping.Bpm > 0 ? mapping.Bpm : 120.0; var clipStartSecs = mapping.SourceRange.Start * 240.0 / bpm; var clipEndSecs = mapping.SourceRange.End * 240.0 / bpm; var xClipStart = EventSecsToScreenX(clipStartSecs); var xClipEnd = EventSecsToScreenX(clipEndSecs); if (xClipStart > min.X) { dl.AddRectFilled(new Vector2(min.X, min.Y), new Vector2(xClipStart, max.Y), _clipOutsideColor); } if (xClipEnd < max.X) { dl.AddRectFilled(new Vector2(xClipEnd, min.Y), new Vector2(max.X, max.Y), _clipOutsideColor); } dl.AddRectFilled(new Vector2(xClipStart, min.Y), new Vector2(xClipStart + 1, max.Y), _clipBoundaryColor); dl.AddRectFilled(new Vector2(xClipEnd, min.Y), new Vector2(xClipEnd + 1, max.Y), _clipBoundaryColor); } const int maxVisibleEvents = 500; const float layerHeight = 20f; var plotCurvePoints = new Vector2[maxVisibleEvents]; _pathTreeDrawer.Reset(); // Draw hovered time var mouseMouse = ImGui.GetMousePos(); if (ImGui.IsWindowHovered()) { var mousePos = mouseMouse; dl.AddRectFilled(new Vector2(mousePos.X, min.Y), new Vector2(mousePos.X + 1, max.Y), UiColors.WidgetActiveLine.Fade(0.1f)); } // Draw Selection range... if (isRangeSelected) { var start = EventSecsToScreenX(_selectRangeStart); var end = EventSecsToScreenX(_selectRangeEnd); if (start > end) { (start, end) = (end, start); } var min1 = new Vector2(start, ImGui.GetWindowPos().Y); var max1 = new Vector2(end, ImGui.GetWindowPos().Y + ImGui.GetWindowHeight()); dl.AddRectFilled(min1, max1, UiColors.ForegroundFull.Fade(0.03f)); } const int filterRecentEventDuration = 10; var dataSetChannels = dataSet.Channels.OrderBy(c => string.Join(".", c.Path)); _activeEventCount = 0; _firstEventTime = double.PositiveInfinity; _lastEventTime = double.NegativeInfinity; foreach (var channel in dataSetChannels) { var newVisibleRange = new ValueRange() { Min = float.PositiveInfinity, Max = float.NegativeInfinity }; var pathString = string.Join(" / ", channel.Path); // This could be more efficient... var channelHash = pathString.GetHashCode(); // Compute random unique color var randomChannelColor = DrawUtils.RandomColorForHash(channelHash); _channelValueRanges.TryGetValue(channelHash, out var valueRange); var lastEvent1 = channel.GetLastEvent(); if (lastEvent1 == null) continue; if (_onlyRecentEvents && ! IsEmbedded) { var lastEvent = lastEvent1; var tooOld = (lastEvent.Time < currentTime - filterRecentEventDuration); if (tooOld) continue; } var isVisible = _pathTreeDrawer.DrawEntry(channel.Path, MaxTreeLevel); if (!isVisible) continue; var isActive = _selectedChannels.Count == 0 || _selectedChannels.Contains(channel); if (isActive) { if (isRangeSelected) { var indexMin = channel.FindIndexForTime(_selectRangeStart); var indexMax = channel.FindIndexForTime(_selectRangeEnd); _activeEventCount += Math.Abs(indexMax - indexMin); } else { _activeEventCount += channel.Events.Count; } } var layerLabelMin = ImGui.GetCursorScreenPos(); var layerMin = new Vector2(ImGui.GetWindowPos().X, layerLabelMin.Y); var layerMax = new Vector2(max.X, layerMin.Y + layerHeight); var layerHovered = ImGui.IsMouseHoveringRect(layerMin, layerMax); if (layerHovered) { dl.AddRectFilled(layerMin, layerMax, UiColors.BackgroundFull.Fade(0.1f)); } var visibleEventCount = 0; var visiblePlotPointCount = 0; var lastX = float.PositiveInfinity; // binary search index with highest visible time var latestVisibleIndex = channel.FindIndexForTime(visibleMaxTime); var oldestVisibleIndex = channel.FindIndexForTime(visibleMinTime, false); var visibleEventCountTotal = latestVisibleIndex - oldestVisibleIndex + 1; var stepSize = MathF.Max((float)visibleEventCountTotal / maxVisibleEvents, 1); if (channel.Events.Count > 0) { lock (channel.Events) { _firstEventTime = Math.Min(_firstEventTime, channel.Events[0]?.Time ?? 0); _lastEventTime = Math.Max(_lastEventTime, channel.Events[^1]?.Time ?? 0); } } var keepSubWindowPos = ImGui.GetCursorScreenPos(); if (ImGui.IsRectVisible(layerMin, layerMax)) { // Draw label flashing with recent events if (channel.Events.Count > 0) { // Shade background dl.AddRectFilled(layerMin + new Vector2(0, 1), layerMin + new Vector2(200, layerHeight), UiColors.WindowBackground.Fade(0.7f) ); dl.AddRectFilledMultiColor(layerMin + new Vector2(200, 1), layerMin + new Vector2(400, layerHeight), UiColors.WindowBackground.Fade(0.7f), UiColors.WindowBackground.Fade(0.0f), UiColors.WindowBackground.Fade(0.0f), UiColors.WindowBackground.Fade(0.7f) ); var lastEventAgeFactor = MathF.Pow((float)(currentTime - lastEvent1.Time).Clamp(0, 1) / 1, 0.2f); var label = channel.Path.Last(); if (!string.IsNullOrEmpty(label)) { var isChannelSelected = _selectedChannels.Contains(channel); var labelColor = Color.Mix(UiColors.ForegroundFull, randomChannelColor.Fade(0.5f), lastEventAgeFactor); var keepPos = ImGui.GetCursorScreenPos(); ImGui.SetCursorScreenPos(layerLabelMin + new Vector2(0, 1)); ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(10, 0)); if (isChannelSelected) { ImGui.PushStyleColor(ImGuiCol.Text, UiColors.BackgroundFull.Rgba); ImGui.PushStyleColor(ImGuiCol.Button, labelColor.Rgba); ImGui.PushStyleColor(ImGuiCol.ButtonHovered, labelColor.Rgba); ImGui.PushStyleColor(ImGuiCol.ButtonActive, labelColor.Rgba); } else { ImGui.PushStyleColor(ImGuiCol.Text, labelColor.Rgba); ImGui.PushStyleColor(ImGuiCol.Button, Color.Transparent.Rgba); ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UiColors.ForegroundFull.Fade(0.1f).Rgba); ImGui.PushStyleColor(ImGuiCol.ButtonActive, UiColors.ForegroundFull.Fade(0.2f).Rgba); } if (ImGui.Button(label)) { if (isChannelSelected) { _selectedChannels.Remove(channel); } else { if (!ImGui.GetIO().KeyCtrl) { _selectedChannels.Clear(); } _selectedChannels.Add(channel); } } ImGui.PopStyleColor(4); ImGui.PopStyleVar(); ImGui.SetCursorScreenPos(keepPos); //ImGui.SameLine(0,0); // dl.AddText(Fonts.FontNormal, // Fonts.FontNormal.FontSize, // layerLabelMin + new Vector2(10, 2), // labelColor, // label); // dl.AddText(Fonts.FontNormal, // Fonts.FontNormal.FontSize, // layerLabelMin + new Vector2(10, 2), // Color.Mix(UiColors.ForegroundFull, randomChannelColor.Fade(0.5f), lastEventAgeFactor), // label); } } //ImGui.SameLine(0,100); ImGui.SetCursorPosX(200); // using (new ChildWindowScope(channel.Path.Last(), // new Vector2(ImGui.GetWindowSize().X - 200, layerHeight), // ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | // ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoBringToFrontOnFocus, // Color.Transparent)) { var dl2 = ImGui.GetWindowDrawList(); // Draw events starting from the end (left to right) for (float fIndex = latestVisibleIndex; fIndex >= 0 //&& fIndex >= latestVisibleIndex - maxVisibleEvents && fIndex >= oldestVisibleIndex && visibleEventCount < maxVisibleEvents; fIndex -= stepSize) { var index = (int)fIndex; var dataEvent = channel.Events[index]; if (dataEvent == null) continue; var msg = ""+dataEvent.Value; float markerYInLayer; var value = float.NaN; if (dataEvent.TryGetNumericValue(out var numericValue)) { value = (float)numericValue; var fNormalized = ((value - valueRange.Min) / (valueRange.Max - valueRange.Min)).Clamp(0, 1); markerYInLayer = (1 - fNormalized) * (layerHeight - 5) + 3; //string msg; if (Math.Abs(value) < 0.0001) msg = "0"; else if (Math.Abs(value) < 10000) { msg = $"{value:G5}"; } else if (Math.Abs(value) < 1000000) { msg = $"{value / 1000:0.0}K"; } else if (Math.Abs(value) < 100000000) { msg = $"{value / 1000000:0.0}M"; } } else { markerYInLayer = layerHeight - 3; } float xStart; // Draw interval events if (dataEvent is DataIntervalEvent intervalEvent) { if (!(intervalEvent.EndTime > visibleMinTime) || !(intervalEvent.Time < visibleMaxTime)) { continue; } xStart = EventSecsToScreenX(intervalEvent.Time); var endTime = intervalEvent.IsUnfinished ? currentTime : intervalEvent.EndTime; var xEnd = MathF.Max(EventSecsToScreenX(endTime), xStart + 1); dl2.AddRectFilled(new Vector2(xStart, layerMin.Y + markerYInLayer), new Vector2(xEnd, layerMax.Y), randomChannelColor.Fade(0.3f)); if (ImGui.IsWindowHovered() && ImGui.IsMouseHoveringRect(new Vector2(xStart, layerMin.Y), new Vector2(xEnd, layerMax.Y))) { ImGui.BeginTooltip(); ImGui.Text($"{msg}\n{dataEvent.Time:0.000s} ... {endTime:0.000s}"); ImGui.EndTooltip(); } } // Draw value events else { if (!(dataEvent.Time > visibleMinTime) || !(dataEvent.Time < visibleMaxTime)) continue; xStart = EventSecsToScreenX(dataEvent.Time); var y = layerMin.Y + markerYInLayer; dl2.AddTriangleFilled(new Vector2(xStart, y - 3), new Vector2(xStart + 2.5f, y + 2), new Vector2(xStart - 2.5f, y + 2), randomChannelColor); plotCurvePoints[visiblePlotPointCount++] = new Vector2(xStart, y); if (ImGui.IsWindowHovered() && ImGui.IsMouseHoveringRect(new Vector2(xStart - 1, layerMin.Y), new Vector2(lastX, layerMax.Y))) { ImGui.BeginTooltip(); ImGui.Text($"{msg}\n{dataEvent.Time:0.000s}"); ImGui.EndTooltip(); } } // Draw label if enough space if(msg != null) { var shortMsg = msg.Length > 20 ? msg.Substring(0, 20) : msg; var gapSize = lastX - xStart; var estimatedWidth = shortMsg.Length * Fonts.FontSmall.FontSize * 0.6f; if (!string.IsNullOrEmpty(shortMsg) && gapSize > 20) { var fade = gapSize.RemapAndClamp(estimatedWidth - 30, estimatedWidth + 60, 0, 1); dl2.AddText(Fonts.FontSmall, Fonts.FontSmall.FontSize, new Vector2(xStart + 8, layerMin.Y + 3), randomChannelColor.Fade(0.6f * fade), shortMsg); } if (!float.IsNaN(value)) { newVisibleRange.Min = MathF.Min(newVisibleRange.Min, value); newVisibleRange.Max = MathF.Max(newVisibleRange.Max, value); } } lastX = xStart; visibleEventCount++; } // Adjust auto height of plot line var newRange = new ValueRange(MathUtils.Lerp(valueRange.Min, newVisibleRange.Min, 0.1f), MathUtils.Lerp(valueRange.Max, newVisibleRange.Max, 0.1f)); if (float.IsNaN(newRange.Min) || float.IsNaN(newRange.Max)) newRange = new ValueRange(); _channelValueRanges[channelHash] = newRange; // Draw plot line if (visiblePlotPointCount > 1) { dl.AddPolyline(ref plotCurvePoints[0], visiblePlotPointCount, randomChannelColor.Fade(0.2f), ImDrawFlags.None, 1); } } //ImGui.EndChild(); ImGui.SetCursorScreenPos(keepSubWindowPos); // Line below layer dl.AddLine(new Vector2(layerMin.X, layerMax.Y), layerMax, UiColors.GridLines.Fade(0.2f)); } layerMin.Y += layerHeight; layerMax.Y += layerHeight; ImGui.SetCursorPosY(ImGui.GetCursorPosY() + layerHeight); } // Handle selection range { if (ImGui.IsWindowHovered() && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { _isDraggingRange = true; _selectRangeStart = ScreenXToEventSecs(mouseMouse.X); } if (_isDraggingRange) { _selectRangeEnd = ScreenXToEventSecs(mouseMouse.X); } if (ImGui.IsMouseReleased(ImGuiMouseButton.Left)) { _isDraggingRange = false; } } // Log.Debug(" TotalVisibleEvents:" + totalVisibleEventCount); ImGui.Dummy(Vector2.One); _pathTreeDrawer.Complete(); // Draw current time var xTime = EventSecsToScreenX(currentTime); dl.AddRectFilled(new Vector2(xTime, min.Y), new Vector2(xTime + 1, max.Y), UiColors.WidgetActiveLine); // Host-supplied overlay (close button, scroll-reset) — must run INSIDE this // Scrollable child or the host's items get blocked by its hit-rect. _embeddedOverlay?.Invoke(); ImGui.EndChild(); ImGui.SetCursorPos(Vector2.Zero); } } private struct ValueRange { public ValueRange(float min, float max) { Min = min; Max = max; } public float Min; public float Max; } private readonly HashSet _selectedChannels = new(); private readonly Dictionary _channelValueRanges = new(); private int _activeEventCount; private bool _onlyRecentEvents = true; private bool _scroll = true; public bool ShowInteraction = true; public int MaxTreeLevel = 3; private double _selectRangeStart; private double _selectRangeEnd; private double _firstEventTime; private double _lastEventTime; private bool _isDraggingRange; // Non-null when the canvas is drawing a timeline-bound DataClip — see the Draw(DataClip) // overload. Re-set on every Draw call so a canvas reused across runtime and clip slots // doesn't leak a stale mapping between frames. private TimeRangeMapping? _mapping; // Non-null when a host pane embeds this view in its own canvas (DrawEmbedded). The // host's canvas owns X transforms / pan / zoom; we just consume its TransformX so // event positions line up. Cleared at the end of every DrawInternal call so a reused // view instance doesn't leak embedded state into a later standalone draw. private ScalableCanvas? _externalCanvas; // Host-supplied overlay invoked inside the Scrollable child — host's only chance to // place widgets (close button, scroll-reset) that are actually hit-testable. Cleared // alongside _externalCanvas at the end of every DrawInternal. private Action? _embeddedOverlay; /// Picks the canvas used for X transforms — host's when embedded, own otherwise. private ScalableCanvas EffectiveCanvas => _externalCanvas ?? _canvas; /// True for the duration of a call. private bool IsEmbedded => _externalCanvas != null; /// /// Maps an event time (source seconds — the unit DataEvent.Time is stored in) to a /// screen X. In standalone mode the own canvas is already scaled in seconds so the /// call is direct; in embedded mode the parent canvas (e.g. TimeLineCanvas) is scaled /// in BARS, so the seconds value has to round-trip through the clip's /// first or events land hundreds of bars off the /// visible region. Inverse symmetric in . /// private float EventSecsToScreenX(double secs) { if (IsEmbedded && _mapping is { } m) return EffectiveCanvas.TransformX((float)m.SourceSecsToLocalBars(secs)); return EffectiveCanvas.TransformX((float)secs); } /// Inverse of . private double ScreenXToEventSecs(float screenX) { if (IsEmbedded && _mapping is { } m) return m.LocalBarsToSourceSecs(EffectiveCanvas.InverseTransformX(screenX)); return EffectiveCanvas.InverseTransformX(screenX); } // Same StatusAnimated palette ClipRange.cs uses for the composition-level TimeClip // overlay so the two surfaces feel like one editor at a glance. private static readonly Color _clipOutsideColor = UiColors.StatusAnimated.Fade(0.02f); private static readonly Color _clipBoundaryColor = UiColors.StatusAnimated.Fade(0.25f); private readonly PathTreeDrawer _pathTreeDrawer = new(); private readonly StandardValueRaster _standardRaster = new() { EnableSnapping = true }; private readonly ScalableCanvas _canvas = new CurrentGraphSubCanvas(initialScale: new Vector2(50, -50)) { FillMode = ScalableCanvas.FillModes.FillAvailableContentRegion, }; private float _scrollPosStart; private bool _isDraggingCanvas; private float _externalScrollXAtDragStart; }