482 lines
19 KiB
C#
482 lines
19 KiB
C#
#nullable enable
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using T3.Core.Operator;
|
|
using T3.Core.Operator.Slots;
|
|
using T3.Core.Settings;
|
|
using T3.Editor.Gui.InputUi.SimpleInputUis;
|
|
|
|
namespace T3.Editor.UiModel.Helpers;
|
|
|
|
/// <summary>
|
|
/// Aggregates information about all symbols (warnings, dependencies, usage, examples),
|
|
/// and also provides single-symbol analysis for dialogs and tools.
|
|
/// </summary>
|
|
internal static class SymbolAnalysis
|
|
{
|
|
/// <summary>
|
|
/// Detailed info per symbol id, filled by UpdateDetails().
|
|
/// </summary>
|
|
internal static readonly Dictionary<Guid, SymbolInformation> InformationForSymbolIds = new(1000);
|
|
|
|
internal static int TotalUsageCount;
|
|
|
|
/// <summary>
|
|
/// All connections between input slots stored as hashes (sourceSlotId x targetSlotId).
|
|
/// Used by SymbolBrowser for relevancy weighting of frequent combinations.
|
|
/// </summary>
|
|
internal static Dictionary<int, int> ConnectionHashCounts = new();
|
|
|
|
internal static bool DetailsInitialized { get; private set; }
|
|
|
|
// Cache for usage counts (used by both bulk and single-symbol queries)
|
|
private static readonly Dictionary<Guid, int> _latestUsageCounts = new();
|
|
|
|
internal static void LogInvalidSymbolDependencies()
|
|
{
|
|
if (!DetailsInitialized)
|
|
UpdateDetails();
|
|
|
|
foreach (var symbol in SymbolRegistry.SymbolsFromAllPackages()
|
|
.OrderBy(s => s.Namespace)
|
|
.ThenBy(s => s.Name))
|
|
{
|
|
if (!InformationForSymbolIds.TryGetValue(symbol.Id, out var infos))
|
|
continue;
|
|
|
|
if (!_packageDependencies.TryGetValue(symbol.SymbolPackage.Name, out var deps))
|
|
continue;
|
|
|
|
foreach (var requiredSymbolId in infos.RequiredSymbolIds)
|
|
{
|
|
if (!SymbolRegistry.TryGetSymbol(requiredSymbolId, out var depSymbol))
|
|
continue;
|
|
|
|
if (!deps.Contains(depSymbol.SymbolPackage.Name))
|
|
{
|
|
Log.Warning($"{symbol.Namespace}.{symbol.Name} depends on {depSymbol.Namespace}.{depSymbol.Name}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
internal static void LogInvalidAssetReference()
|
|
{
|
|
foreach (var symbol in SymbolRegistry.SymbolsFromAllPackages()
|
|
.OrderBy(s => s.Namespace)
|
|
.ThenBy(s => s.Name))
|
|
{
|
|
if (!_packageDependencies.TryGetValue(symbol.SymbolPackage.Name, out var deps))
|
|
continue;
|
|
|
|
|
|
|
|
// Symbol children
|
|
foreach (var child in symbol.Children.Values)
|
|
{
|
|
foreach (var input in child.Inputs.Values)
|
|
{
|
|
if (input.IsDefault)
|
|
continue;
|
|
|
|
if (input.InputDefinition.ValueType != typeof(string))
|
|
continue;
|
|
|
|
if (input.Value is not InputValue<string> stringValue)
|
|
continue;
|
|
|
|
var pathValue = stringValue.Value;
|
|
if (string.IsNullOrEmpty(pathValue))
|
|
continue;
|
|
|
|
if (!SymbolUiRegistry.TryGetSymbolUi(child.Symbol.Id, out var childSymbolUi))
|
|
continue;
|
|
|
|
if (!childSymbolUi.InputUis.TryGetValue(input.Id, out var inputUi))
|
|
continue;
|
|
|
|
if (inputUi is not StringInputUi stringUi)
|
|
continue;
|
|
|
|
|
|
switch (stringUi.Usage)
|
|
{
|
|
case StringInputUi.UsageType.FilePath:
|
|
case StringInputUi.UsageType.DirectoryPath:
|
|
{
|
|
var assetPackageNameIndex = pathValue.IndexOf(':');
|
|
if (assetPackageNameIndex < 1)
|
|
break;
|
|
|
|
var assetPackageName = pathValue[..(assetPackageNameIndex)];
|
|
|
|
if (!deps.Contains(assetPackageName))
|
|
{
|
|
Log.Warning($"{symbol.Namespace}.[{symbol.Name}].{childSymbolUi.Symbol.Name} references {pathValue}");
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private static readonly string[] _libPackages = [FileLocations.LibPackageName, FileLocations.TypesPackageName, "Mediapipe", "t3.ndi", "t3.spout", "t3.unsplash"];
|
|
|
|
private static readonly Dictionary<string, string[]> _packageDependencies
|
|
= new()
|
|
{
|
|
{ FileLocations.LibPackageName, [.._libPackages] },
|
|
{ FileLocations.ExamplesPackageName, [.._libPackages, FileLocations.ExamplesPackageName] },
|
|
{ FileLocations.SkillsPackageName, [.._libPackages, FileLocations.SkillsPackageName] }
|
|
};
|
|
|
|
/// <summary>
|
|
/// Basic usage info used by symbol browser and relevancy search.
|
|
/// </summary>
|
|
internal static void UpdateSymbolUsageCounts()
|
|
{
|
|
Log.Debug("Updating symbol counts...");
|
|
var usages = CollectSymbolUsageCounts();
|
|
ConnectionHashCounts = new Dictionary<int, int>();
|
|
|
|
foreach (var symbolUi in EditorSymbolPackage.AllSymbolUis)
|
|
{
|
|
var symbolId = symbolUi.Symbol.Id;
|
|
if (!InformationForSymbolIds.TryGetValue(symbolId, out var info))
|
|
{
|
|
info = new SymbolInformation();
|
|
InformationForSymbolIds[symbolId] = info;
|
|
}
|
|
|
|
// Update connection counts
|
|
foreach (var connection in symbolUi.Symbol.Connections)
|
|
{
|
|
var hash = connection.SourceSlotId.GetHashCode() * 31
|
|
+ connection.TargetSlotId.GetHashCode();
|
|
ConnectionHashCounts.TryGetValue(hash, out var connectionCount);
|
|
ConnectionHashCounts[hash] = connectionCount + 1;
|
|
}
|
|
|
|
usages.TryGetValue(symbolId, out var count);
|
|
info.UsageCount = count;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update <see cref="InformationForSymbolIds"/> collection with details useful for
|
|
/// library structure cleanup, browsing, etc.
|
|
/// </summary>
|
|
internal static void UpdateDetails()
|
|
{
|
|
var usages = CollectSymbolUsageCounts();
|
|
InformationForSymbolIds.Clear();
|
|
TotalUsageCount = 0;
|
|
|
|
// Precompute reverse dependencies once for all symbols
|
|
var (_, reverseDeps) = CollectUsageAndReverseDependencies();
|
|
|
|
foreach (var symbolUi in EditorSymbolPackage.AllSymbolUis)
|
|
{
|
|
var symbol = symbolUi.Symbol;
|
|
usages.TryGetValue(symbol.Id, out var usageCount);
|
|
TotalUsageCount += usageCount;
|
|
|
|
var requiredSymbols = CollectRequiredSymbols(symbol);
|
|
var invalidRequirements = CollectInvalidRequirements(symbol, requiredSymbols).ToList();
|
|
|
|
reverseDeps.TryGetValue(symbol.Id, out var deps);
|
|
var dependingSymbols = deps ?? new HashSet<Guid>();
|
|
|
|
InformationForSymbolIds[symbol.Id] = BuildSymbolInformation(
|
|
symbol,
|
|
symbolUi,
|
|
requiredSymbols,
|
|
invalidRequirements,
|
|
dependingSymbols,
|
|
usageCount);
|
|
}
|
|
|
|
DetailsInitialized = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fast single-symbol analysis for dialogs, menus, etc.
|
|
/// Uses cached bulk data when available, otherwise computes on-demand.
|
|
/// </summary>
|
|
/// <param name="symbol">The symbol to analyze.</param>
|
|
/// <param name="info">Returns the collected information for the symbol.</param>
|
|
/// <param name="forceUpdate">
|
|
/// If true, forces a fresh analysis for this symbol even if cached data is available.
|
|
/// If false, uses the cached information when possible.
|
|
/// </param>
|
|
internal static bool TryGetSymbolInfo(Symbol symbol, out SymbolInformation info, bool forceUpdate = false)
|
|
{
|
|
info = new SymbolInformation();
|
|
|
|
if (!symbol.TryGetSymbolUi(out var symbolUi))
|
|
return false;
|
|
|
|
// If UpdateDetails has run and no force update is requested, return the cached info
|
|
if (!forceUpdate && DetailsInitialized && InformationForSymbolIds.TryGetValue(symbol.Id, out var cached))
|
|
{
|
|
// Keep a copy so callers cannot accidentally mutate the cache.
|
|
info = cached;
|
|
|
|
// Keep usage count fresh if we have a newer value
|
|
if (_latestUsageCounts.TryGetValue(symbol.Id, out var usage))
|
|
info.UsageCount = usage;
|
|
|
|
return true;
|
|
}
|
|
|
|
// On-demand analysis for this single symbol
|
|
var requiredSymbols = CollectRequiredSymbols(symbol);
|
|
var invalidRequirements = CollectInvalidRequirements(symbol, requiredSymbols).ToList();
|
|
var (usageCounts, reverseDeps) = CollectUsageAndReverseDependencies();
|
|
|
|
usageCounts.TryGetValue(symbol.Id, out var usageCount);
|
|
reverseDeps.TryGetValue(symbol.Id, out var deps);
|
|
var dependingSymbols = deps ?? new HashSet<Guid>();
|
|
|
|
info = BuildSymbolInformation(
|
|
symbol,
|
|
symbolUi,
|
|
requiredSymbols,
|
|
invalidRequirements,
|
|
dependingSymbols,
|
|
usageCount);
|
|
|
|
// Optionally refresh the cached entry if details are already initialized
|
|
// so that subsequent calls without forceUpdate can benefit.
|
|
if (DetailsInitialized)
|
|
{
|
|
InformationForSymbolIds[symbol.Id] = info;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unified structure with all known symbol metadata.
|
|
/// </summary>
|
|
public sealed class SymbolInformation
|
|
{
|
|
public List<string> Warnings = [];
|
|
internal HashSet<Guid> RequiredSymbolIds = [];
|
|
internal HashSet<Guid> DependingSymbols = [];
|
|
public List<Guid> InvalidRequiredIds = [];
|
|
public IReadOnlyList<Guid> ExampleSymbolsIds = [];
|
|
internal int UsageCount;
|
|
internal bool LacksDescription;
|
|
internal bool LacksAllParameterDescription;
|
|
internal bool LacksSomeParameterDescription;
|
|
internal bool LacksParameterGrouping;
|
|
internal bool DependsOnObsoleteOps;
|
|
internal SymbolUi.SymbolTags Tags; // Copy to avoid reference to symbolUi
|
|
internal OperatorClassification OperatorType;
|
|
}
|
|
|
|
public enum OperatorClassification
|
|
{
|
|
Unknown = 0,
|
|
Lib,
|
|
Type,
|
|
Example,
|
|
T3,
|
|
Skill,
|
|
}
|
|
|
|
// Shared helpers (used by both bulk and single analysis)
|
|
#region Shared Helpers
|
|
internal static bool TryGetOperatorType(Symbol symbol, out OperatorClassification opType)
|
|
{
|
|
var ns = symbol.Namespace ?? string.Empty;
|
|
var rootSegment = ns.Split('.')[0];
|
|
|
|
opType = rootSegment switch
|
|
{
|
|
FileLocations.LibPackageName => OperatorClassification.Lib,
|
|
FileLocations.TypesPackageName => OperatorClassification.Type,
|
|
FileLocations.ExamplesPackageName => OperatorClassification.Example,
|
|
"t3" => OperatorClassification.T3,
|
|
FileLocations.SkillsPackageName => OperatorClassification.Skill,
|
|
_ => OperatorClassification.Unknown
|
|
};
|
|
return opType != OperatorClassification.Unknown;
|
|
}
|
|
|
|
private static SymbolInformation BuildSymbolInformation(Symbol symbol,
|
|
SymbolUi symbolUi,
|
|
HashSet<Symbol> requiredSymbols,
|
|
List<Guid> invalidRequirements,
|
|
HashSet<Guid> dependingSymbols,
|
|
int usageCount)
|
|
{
|
|
var inputUis = symbolUi.InputUis.Values;
|
|
|
|
var lacksDescription = string.IsNullOrWhiteSpace(symbolUi.Description);
|
|
var hasManyInputs = symbolUi.InputUis.Count > 2;
|
|
var lacksAllParamDesc = hasManyInputs &&
|
|
inputUis.All(i => string.IsNullOrWhiteSpace(i.Description));
|
|
var lacksSomeParamDesc = hasManyInputs &&
|
|
inputUis.Any(i => string.IsNullOrWhiteSpace(i.Description));
|
|
|
|
var lacksParameterGrouping = symbolUi.InputUis.Count > 4 &&
|
|
!inputUis.Any(i => i.AddPadding || !string.IsNullOrEmpty(i.GroupTitle));
|
|
|
|
var dependsOnObsolete = requiredSymbols
|
|
.Select(s => s.GetSymbolUi())
|
|
.Any(ui => ui != null && ui.Tags.HasFlag(SymbolUi.SymbolTags.Obsolete));
|
|
|
|
TryGetOperatorType(symbol, out var opType);
|
|
|
|
return new SymbolInformation
|
|
{
|
|
Warnings = new List<string>(),
|
|
RequiredSymbolIds = requiredSymbols.Select(s => s.Id).ToHashSet(),
|
|
DependingSymbols = dependingSymbols,
|
|
InvalidRequiredIds = invalidRequirements,
|
|
ExampleSymbolsIds = ExampleSymbolLinking.GetExampleIds(symbol.Id),
|
|
UsageCount = usageCount,
|
|
LacksDescription = lacksDescription,
|
|
LacksAllParameterDescription = lacksAllParamDesc,
|
|
LacksSomeParameterDescription = lacksSomeParamDesc,
|
|
LacksParameterGrouping = lacksParameterGrouping,
|
|
DependsOnObsoleteOps = dependsOnObsolete,
|
|
Tags = symbolUi.Tags,
|
|
OperatorType = opType
|
|
};
|
|
}
|
|
|
|
private static HashSet<Symbol> CollectRequiredSymbols(Symbol root)
|
|
{
|
|
var all = new HashSet<Symbol>();
|
|
Collect(root);
|
|
return all;
|
|
|
|
void Collect(Symbol symbol)
|
|
{
|
|
foreach (var symbolChild in symbol.Children.Values)
|
|
{
|
|
if (!all.Add(symbolChild.Symbol))
|
|
continue;
|
|
Collect(symbolChild.Symbol);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Collect Ids of required symbols that are not within the list of projects
|
|
/// </summary>
|
|
private static IEnumerable<Guid> CollectInvalidRequirements(Symbol root, HashSet<Symbol> requiredSymbols)
|
|
{
|
|
var result = new List<Symbol>();
|
|
|
|
// Todo: implement this correctly?
|
|
HashSet<string> validPackagesNames = new()
|
|
{
|
|
FileLocations.TypesPackageName,
|
|
FileLocations.LibPackageName,
|
|
FileLocations.ExamplesPackageName,
|
|
root.SymbolPackage.RootNamespace,
|
|
};
|
|
|
|
foreach (var r in requiredSymbols)
|
|
{
|
|
var projectId = r.SymbolPackage.RootNamespace;
|
|
if (validPackagesNames.Contains(projectId))
|
|
continue;
|
|
|
|
result.Add(r);
|
|
}
|
|
|
|
return result
|
|
.OrderBy(s => s.Namespace)
|
|
.ThenBy(s => s.Name)
|
|
.Select(s => s.Id);
|
|
}
|
|
|
|
private static Dictionary<Guid, int> CollectSymbolUsageCounts()
|
|
{
|
|
var results = new Dictionary<Guid, int>();
|
|
TotalUsageCount = 0;
|
|
|
|
foreach (var s in EditorSymbolPackage.AllSymbols)
|
|
{
|
|
foreach (var child in s.Children.Values)
|
|
{
|
|
results.TryGetValue(child.Symbol.Id, out var currentCount);
|
|
results[child.Symbol.Id] = currentCount + 1;
|
|
TotalUsageCount++;
|
|
}
|
|
}
|
|
|
|
_latestUsageCounts.Clear();
|
|
foreach (var kvp in results)
|
|
_latestUsageCounts[kvp.Key] = kvp.Value;
|
|
|
|
return results;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds per-symbol usage counts and a reverse dependency map in a single pass
|
|
/// over <see cref="EditorSymbolPackage.AllSymbols"/>.
|
|
/// This is used by single-symbol analysis without running the full detail update.
|
|
/// </summary>
|
|
private static (Dictionary<Guid, int> UsageCounts, Dictionary<Guid, HashSet<Guid>> ReverseDependencies)
|
|
CollectUsageAndReverseDependencies()
|
|
{
|
|
var usageCounts = new Dictionary<Guid, int>();
|
|
var reverseDeps = new Dictionary<Guid, HashSet<Guid>>();
|
|
|
|
foreach (var container in EditorSymbolPackage.AllSymbols)
|
|
{
|
|
var containerId = container.Id;
|
|
foreach (var child in container.Children.Values)
|
|
{
|
|
var childId = child.Symbol.Id;
|
|
usageCounts.TryGetValue(childId, out var current);
|
|
usageCounts[childId] = current + 1;
|
|
|
|
if (!reverseDeps.TryGetValue(childId, out var set))
|
|
{
|
|
set = new HashSet<Guid>();
|
|
reverseDeps[childId] = set;
|
|
}
|
|
|
|
set.Add(containerId);
|
|
}
|
|
}
|
|
|
|
return (usageCounts, reverseDeps);
|
|
}
|
|
#endregion
|
|
|
|
public static bool TryGetFileInputFromInstance(Instance instance,
|
|
[NotNullWhen(true)] out InputSlot<string>? stringInput,
|
|
[NotNullWhen(true)] out StringInputUi? stringInputUi)
|
|
{
|
|
stringInput = null;
|
|
stringInputUi = null;
|
|
|
|
var symbolUi = instance.GetSymbolUi();
|
|
foreach (var input in instance.Inputs)
|
|
{
|
|
if (input is not InputSlot<string> tmpStringInput)
|
|
continue;
|
|
|
|
stringInput = tmpStringInput;
|
|
|
|
var inputUi = symbolUi.InputUis[input.Id];
|
|
if (inputUi is not StringInputUi { Usage: StringInputUi.UsageType.FilePath } tmpStringInputUi)
|
|
continue;
|
|
|
|
stringInputUi = tmpStringInputUi;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
} |