#nullable enable using T3.Core.Animation; using T3.Core.DataTypes.DataSet; using T3CoreDataClip = T3.Core.DataTypes.DataSet.DataClip; namespace Lib.io.data; /// /// Loads a recorded .data file and presents it as a timeline-bound /// . Owns the load + /// placement, then publishes the result as a /// single output. Downstream ops consume the clip and read /// .Set for content and .Mapping for timing. /// /// /// Loading flows through the shared , so multiple /// LoadDataClip ops referencing the same file share one parsed /// . The Resource<DataSet> wrapper additionally gives /// file-watch invalidation when the source is re-recorded. /// [Guid("4d1c0e80-7b2a-4f6d-9c1b-12d3e4f50607")] internal sealed class LoadDataClip : Instance, IStatusProvider, IDescriptiveFilename { [Output(Guid = "5e2d1f81-8c3b-4a7e-ad2c-23e4f5061718", DirtyFlagTrigger = DirtyFlagTrigger.Animated)] public readonly TimeClipSlot Clip = new(); public LoadDataClip() { _dataSetResource = new Resource(FilePath, TryLoad); _dataSetResource.AddDependentSlots(Clip); Clip.UpdateAction += Update; } private bool TryLoad(FileResource file, DataSet? currentValue, [NotNullWhen(true)] out DataSet? newValue, [NotNullWhen(false)] out string? failureReason) { // Let the cache decide whether to re-parse. It already keys entries by file path // + last-write timestamp, so a real file change forces a fresh parse; otherwise // every LoadDataClip pointing at the same path shares one DataSet instance. // That sharing matters for in-memory mutations (e.g. "Remove channel" in the // DataSet output view): a Split that creates a sibling LoadDataClip used to // wipe those edits because we explicitly invalidated here on every TryLoad, // re-parsing the on-disk file and resurrecting the removed channels. var absolutePath = file.AbsolutePath; if (!DataSetCache.TryGet(absolutePath, out var loaded, out var reason)) { newValue = null; failureReason = reason; _errorMessageForStatus = reason; return false; } newValue = loaded; failureReason = null; _errorMessageForStatus = string.Empty; return true; } private void Update(EvaluationContext context) { var dataSet = _dataSetResource.GetValue(context); if (dataSet == null) { Clip.Value = null; return; } // SourceRange is in file-time (bars), TimeRange in timeline-time (bars). // Recording sets SourceRange = (0, durationBars) — file events are anchored to // record-start (file-time 0). Cut / drag-start trim adjust SourceRange in // file-time directly, so the mapping is a straight identity build with no // rebasing. Same convention as PlayVideoClip / MidiClip — keeps split, trim, // and body-drag math consistent across clip types. var timeRange = Clip.TimeClip.TimeRange; var sourceRange = Clip.TimeClip.SourceRange; var mapping = new TimeRangeMapping(timeRange, sourceRange, context.Playback.Bpm); Clip.Value = new T3CoreDataClip { Set = dataSet, Mapping = mapping, }; Clip.DirtyFlag.Clear(); } IStatusProvider.StatusLevel IStatusProvider.GetStatusLevel() => string.IsNullOrEmpty(_errorMessageForStatus) ? IStatusProvider.StatusLevel.Success : IStatusProvider.StatusLevel.Warning; string IStatusProvider.GetStatusMessage() => _errorMessageForStatus; public IEnumerable FileFilter => _fileFilter; public InputSlot SourcePathSlot => FilePath; private static readonly string[] _fileFilter = ["*.data"]; [Input(Guid = "70419103-ae5d-4ca0-cf4e-456071829304")] public readonly InputSlot FilePath = new(); private readonly Resource _dataSetResource; private string _errorMessageForStatus = string.Empty; }