585 lines
22 KiB
C#
585 lines
22 KiB
C#
#nullable enable
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.IO;
|
|
using T3.Core.Compilation;
|
|
using T3.Core.DataTypes;
|
|
using T3.Core.IO;
|
|
using T3.Core.Operator;
|
|
using T3.Core.Operator.Slots;
|
|
using T3.Core.Settings;
|
|
using T3.Core.Resource;
|
|
using T3.Core.Resource.Assets;
|
|
using T3.Core.SystemUi;
|
|
using T3.Editor.Compilation;
|
|
using T3.Editor.Gui;
|
|
using T3.Editor.Gui.InputUi.SimpleInputUis;
|
|
using T3.Editor.Gui.Interaction.Timing;
|
|
using T3.Serialization;
|
|
|
|
namespace T3.Editor.UiModel.Exporting;
|
|
|
|
internal static partial class PlayerExporter
|
|
{
|
|
public static bool TryExportInstance(Instance composition, SymbolUi.Child childUi, out string reason, out string exportDir)
|
|
{
|
|
T3Ui.Save(false);
|
|
|
|
// Collect all ops and types
|
|
var exportedInstance = composition.Children[childUi.SymbolChild.Id];
|
|
var symbol = exportedInstance.Symbol;
|
|
Log.Info($"Exporting {symbol.Name}...");
|
|
|
|
var output = exportedInstance.Outputs.FirstOrDefault();
|
|
if (output == null || output.ValueType != typeof(Texture2D))
|
|
{
|
|
reason = "Can only export ops with 'Texture2D' output";
|
|
exportDir = string.Empty;
|
|
return false;
|
|
}
|
|
|
|
// Traverse starting at output and collect everything
|
|
var exportData = new ExportData();
|
|
exportData.TryAddSymbol(symbol);
|
|
|
|
var package = composition.Symbol.SymbolPackage;
|
|
exportDir = Path.Combine(package.Folder, FileLocations.ExportSubFolder, childUi.SymbolChild.ReadableName);
|
|
|
|
if (!TryRemoveExistingExportDir(out reason, exportDir))
|
|
return false;
|
|
|
|
Directory.CreateDirectory(exportDir);
|
|
|
|
var operatorDir = Path.Combine(exportDir, FileLocations.OperatorsSubFolder);
|
|
Directory.CreateDirectory(operatorDir);
|
|
|
|
// Copy assemblies into export dir. Get symbol packages directly used by the exported symbols
|
|
if (!TryExportSymbolPackages(out reason, exportData, operatorDir))
|
|
return false;
|
|
|
|
// Get soundtrack or show warning message
|
|
if (TryFindSoundtrack(exportedInstance, symbol, out var address))
|
|
{
|
|
if (AssetRegistry.TryGetAsset(address, out var soundtrackAsset))
|
|
{
|
|
exportData.TryAddSharedAsset(soundtrackAsset);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const string yes = "Yes";
|
|
var choice = BlockingWindow.Instance.ShowMessageBox("No defined soundtrack found. Continue with export?", "No soundtrack", yes,
|
|
"No, cancel export");
|
|
|
|
if (choice != yes)
|
|
{
|
|
reason = $"Failed to find soundTrack for [{symbol.Name}] - export cancelled, see log for details";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Include implicitly shared assets
|
|
foreach (var shared in (string[]) [
|
|
"Lib:shaders/dx11/resolve-multisampled-depth-buffer-cs.hlsl",
|
|
"Lib:pbr/studio_small_08-prefiltered.dds",
|
|
"Lib:pbr/BRDF-LookUp.dds",
|
|
])
|
|
{
|
|
if (AssetRegistry.TryGetAsset(shared, out var soundtrackAsset))
|
|
{
|
|
exportData.TryAddSharedAsset(soundtrackAsset);
|
|
}
|
|
else
|
|
{
|
|
Log.Warning("Can't resolved shared asset " + shared);
|
|
}
|
|
}
|
|
|
|
// Collect used assets
|
|
RecursivelyCollectExportData(output, exportData);
|
|
exportData.PrintInfo();
|
|
|
|
if (!AssetExportItem.TryCopyItems(exportData.ExportItems, exportDir))
|
|
{
|
|
reason = "Failed to copy resource files - see log for details";
|
|
return false;
|
|
}
|
|
|
|
if (!TryExportAssetsOnlyPackages(exportData, operatorDir, out reason))
|
|
return false;
|
|
|
|
// Copy shared assets
|
|
var editorResourcesTargetDir = Path.Combine(exportDir, FileLocations.EditorResourcesSubfolder);
|
|
Directory.CreateDirectory(editorResourcesTargetDir);
|
|
if (!TryCopyDirectory(SharedResources.EditorResourcesDirectory, editorResourcesTargetDir, out reason))
|
|
return false;
|
|
|
|
// Exclude optional dependency files that are not required by any exported symbol.
|
|
var excludedDependencyFiles = GetUnusedMappedDependencyFiles(exportData);
|
|
var playerDirectory = Path.Combine(FileLocations.StartFolder, "Player");
|
|
if (!TryCopyDirectory(playerDirectory,
|
|
exportDir,
|
|
out reason,
|
|
excludeFiles: excludedDependencyFiles))
|
|
return false;
|
|
|
|
if (!TryExportSettings(exportDir, symbol, out reason))
|
|
return false;
|
|
|
|
reason = "Exported successfully to " + exportDir;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Compile EditableProjects and copy read only projects.
|
|
/// </summary>
|
|
private static bool TryExportSymbolPackages(out string reason, ExportData exportData, string operatorDir)
|
|
{
|
|
string[] excludeSubdirectories =
|
|
[
|
|
".git",
|
|
FileLocations.SymbolUiSubFolder,
|
|
FileLocations.SourceCodeSubFolder,
|
|
FileLocations.ExportSubFolder,
|
|
FileLocations.AssetsSubfolder, // Assets are filtered by referencing address and copied separately
|
|
];
|
|
|
|
foreach (var package in exportData.SymbolPackages)
|
|
{
|
|
Log.Debug($"Exporting package {package.Name}...");
|
|
var packageName = package.Name;
|
|
var targetDirectory = Path.Combine(operatorDir, packageName);
|
|
Directory.CreateDirectory(targetDirectory);
|
|
|
|
if (package is EditableSymbolProject project)
|
|
{
|
|
project.SaveModifiedSymbols();
|
|
if (!project.CsProjectFile.TryCompileRelease(false, out var failureLog))
|
|
{
|
|
reason = $"Failed to compile project \"{packageName}\" - \n{failureLog}";
|
|
return false;
|
|
}
|
|
|
|
// Copy the resulting directory into the target directory
|
|
var sourceDir = project.CsProjectFile.GetBuildTargetDirectory(CsProjectFile.PlayerBuildMode);
|
|
|
|
// Copy contents recursively into the target directory
|
|
if (!TryCopyDirectory(sourceDir, targetDirectory, out reason, excludeSubdirectories))
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
// Copy full directory into target directory recursively, maintaining folder layout
|
|
var directoryToCopy = package.AssemblyInformation.Directory;
|
|
|
|
if (!TryCopyDirectory(directoryToCopy, targetDirectory, out reason, excludeSubdirectories))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
reason = string.Empty;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// If only Assets but no Symbols are used from a package, we still need to copy its OperatorPackage.json file,
|
|
/// so the player can register these assets on startup.
|
|
/// </summary>
|
|
private static bool TryExportAssetsOnlyPackages(ExportData exportData, string operatorDir, out string reason)
|
|
{
|
|
reason = string.Empty;
|
|
foreach (var assetPackage in exportData.AssetPackages)
|
|
{
|
|
var alreadyIncluded = exportData.SymbolPackages.Any(sp => sp.Id == assetPackage.Id);
|
|
if (alreadyIncluded)
|
|
continue;
|
|
|
|
var sourcePath = Path.Combine(assetPackage.Folder, ReleaseInfo.FileName);
|
|
var targetPath = Path.Combine(operatorDir, assetPackage.Name, ReleaseInfo.FileName);
|
|
|
|
if (!TryCopyFile(sourcePath, targetPath))
|
|
{
|
|
reason = $"Failed to copy {sourcePath} for asset package {assetPackage}";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Recursively copies a directory to a target directory, excluding specified subfolders, files, and file extensions.
|
|
/// </summary>
|
|
private static bool TryCopyDirectory(string directoryToCopy, string targetDirectory, out string reason, string[]? excludeSubFolders = null,
|
|
string[]? excludeFiles = null, string[]? excludeFileExtensions = null)
|
|
{
|
|
try
|
|
{
|
|
var rootFiles = Directory.EnumerateFiles(directoryToCopy, "*", SearchOption.TopDirectoryOnly);
|
|
var subfolderFiles = Directory.EnumerateDirectories(directoryToCopy, "*", SearchOption.TopDirectoryOnly)
|
|
.Where(subDir =>
|
|
{
|
|
if (excludeSubFolders == null)
|
|
return true;
|
|
|
|
var dirName = Path.GetRelativePath(directoryToCopy, subDir);
|
|
foreach (var excludeSubFolder in excludeSubFolders)
|
|
{
|
|
if (string.Equals(dirName, excludeSubFolder, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
})
|
|
.SelectMany(subDir => Directory.EnumerateFiles(subDir, "*", SearchOption.AllDirectories));
|
|
|
|
var files = rootFiles.Concat(subfolderFiles);
|
|
var shouldExcludeFiles = excludeFiles != null;
|
|
var shouldExcludeFileExtensions = excludeFileExtensions != null;
|
|
foreach (var file in files)
|
|
{
|
|
if (shouldExcludeFiles && excludeFiles!.Contains(Path.GetFileName(file)))
|
|
continue;
|
|
|
|
bool shouldSkipBasedOnExtension = false;
|
|
if (shouldExcludeFileExtensions)
|
|
{
|
|
foreach (var extension in excludeFileExtensions!)
|
|
{
|
|
if (file.EndsWith(extension, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
shouldSkipBasedOnExtension = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (shouldSkipBasedOnExtension)
|
|
continue;
|
|
|
|
var relativePath = Path.GetRelativePath(directoryToCopy, file);
|
|
var targetPath = Path.Combine(targetDirectory, relativePath);
|
|
var targetDir = Path.GetDirectoryName(targetPath);
|
|
if (targetDir == null)
|
|
{
|
|
reason = $"Failed to get directory for \"{targetPath}\" - is it missing a file extension?";
|
|
return false;
|
|
}
|
|
|
|
Directory.CreateDirectory(targetDir);
|
|
File.Copy(file, targetPath, true);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
reason = $"Failed to copy directory {directoryToCopy} to {targetDirectory}. Exception:\n{e}";
|
|
return false;
|
|
}
|
|
|
|
reason = string.Empty;
|
|
return true;
|
|
}
|
|
|
|
private static bool TryCopyFile(string sourcePath, string targetPath)
|
|
{
|
|
var directory = Path.GetDirectoryName(targetPath);
|
|
try
|
|
{
|
|
Directory.CreateDirectory(directory!);
|
|
File.Copy(sourcePath, targetPath, true);
|
|
return true;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.Error($"Failed to copy resource file for export: {sourcePath} {e.Message}");
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static void RecursivelyCollectExportData(ISlot slot, ExportData exportData)
|
|
{
|
|
var gotConnection = slot.TryGetFirstConnection(out var firstConnection);
|
|
if (slot is IInputSlot)
|
|
{
|
|
if (gotConnection)
|
|
{
|
|
RecursivelyCollectExportData(firstConnection, exportData);
|
|
}
|
|
|
|
CheckInputForResourcePath(slot, exportData);
|
|
return;
|
|
}
|
|
|
|
if (gotConnection)
|
|
{
|
|
// slot is an output of an composition op
|
|
RecursivelyCollectExportData(firstConnection, exportData);
|
|
exportData.TryAddInstance(slot.Parent);
|
|
return;
|
|
}
|
|
|
|
var parent = slot.Parent;
|
|
|
|
if (!exportData.TryAddInstance(parent))
|
|
return; // already visited
|
|
|
|
foreach (var input in parent.Inputs)
|
|
{
|
|
CheckInputForResourcePath(input, exportData);
|
|
|
|
if (!input.HasInputConnections)
|
|
continue;
|
|
|
|
if (input.TryGetAsMultiInput(out var multiInput))
|
|
{
|
|
foreach (var entry in multiInput.GetCollectedInputs())
|
|
{
|
|
RecursivelyCollectExportData(entry, exportData);
|
|
}
|
|
}
|
|
else if (input.TryGetFirstConnection(out var inputsFirstConnection))
|
|
{
|
|
RecursivelyCollectExportData(inputsFirstConnection, exportData);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool TryFindSoundtrack(Instance instance, Symbol symbol,
|
|
[NotNullWhen(true)] out string? address)
|
|
{
|
|
var playbackSettings = symbol.CompositionSettings;
|
|
if (playbackSettings == null)
|
|
{
|
|
Log.Warning($"Project {symbol} has no playback settings");
|
|
address = null;
|
|
return false;
|
|
}
|
|
|
|
if (playbackSettings.TryGetMainSoundtrack(instance, out var soundtrack) is not true)
|
|
{
|
|
if (PlaybackUtils.TryFindingSoundtrack(out soundtrack, out _))
|
|
{
|
|
Log.Warning($"You should define soundtracks withing the exported operators. Falling back to {soundtrack.Clip.AssetPath} set in parent...");
|
|
}
|
|
else
|
|
{
|
|
address = null;
|
|
return false;
|
|
}
|
|
|
|
Log.Debug("No soundtrack defined within operator.");
|
|
}
|
|
|
|
address = soundtrack.Clip.AssetPath;
|
|
return FileResource.TryGetFileResource(soundtrack.Clip.AssetPath, instance, out _);
|
|
}
|
|
|
|
private static void CheckInputForResourcePath(ISlot inputSlot, ExportData exportData)
|
|
{
|
|
var parent = inputSlot.Parent;
|
|
var inputUi = parent.GetSymbolUi().InputUis[inputSlot.Id];
|
|
if (inputUi is not StringInputUi stringInputUi)
|
|
return;
|
|
|
|
if (stringInputUi.Usage != StringInputUi.UsageType.FilePath && stringInputUi.Usage != StringInputUi.UsageType.DirectoryPath)
|
|
return;
|
|
|
|
var compositionSymbol = parent.Parent?.Symbol;
|
|
if (compositionSymbol == null)
|
|
return;
|
|
|
|
var parentSymbolChild = compositionSymbol.Children[parent.SymbolChildId];
|
|
var value = parentSymbolChild.Inputs[inputSlot.Id].Value;
|
|
if (value is not InputValue<string> stringValue)
|
|
return;
|
|
|
|
var address = stringValue.Value;
|
|
|
|
switch (stringInputUi.Usage)
|
|
{
|
|
case StringInputUi.UsageType.FilePath:
|
|
{
|
|
if (!AssetRegistry.TryGetAsset(address, out var asset))
|
|
{
|
|
Log.Warning($" Asset not found '{address}'");
|
|
break;
|
|
}
|
|
|
|
exportData.TryAddSharedAsset(asset);
|
|
break;
|
|
}
|
|
case StringInputUi.UsageType.DirectoryPath:
|
|
{
|
|
//var relativeDirectory = stringValue.Value;
|
|
//var isFolder = relativeDirectory.EndsWith('/');
|
|
|
|
if (!AssetRegistry.TryResolveAddress(address, parent, out var absoluteDirectory, out var package, isFolder: true))
|
|
{
|
|
Log.Warning($" Directory '{address}' was not found in any resource folder");
|
|
break;
|
|
}
|
|
|
|
// if (package == null)
|
|
// {
|
|
// Log.Warning($"Directory '{address}' can't be exported without a package");
|
|
// break;
|
|
// }
|
|
|
|
Log.Debug($"Export all files in folder {absoluteDirectory}...");
|
|
foreach (var absolutePath in Directory.EnumerateFiles(absoluteDirectory, "*", SearchOption.AllDirectories))
|
|
{
|
|
var relativePathInResourceFolder = Path.GetRelativePath(package.AssetsFolder, absolutePath);
|
|
|
|
exportData.TryAddExportAsset(new AssetExportItem(package.RootNamespace,
|
|
relativePathInResourceFolder,
|
|
absolutePath));
|
|
}
|
|
|
|
break;
|
|
}
|
|
case StringInputUi.UsageType.Default:
|
|
case StringInputUi.UsageType.Multiline:
|
|
case StringInputUi.UsageType.CustomDropdown:
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static bool TryExportSettings(string exportDir, Symbol symbol, out string reason)
|
|
{
|
|
reason = string.Empty;
|
|
|
|
// Update project settings
|
|
var exportSettings = new ExportSettings(OperatorId: symbol.Id,
|
|
ApplicationTitle: symbol.Name,
|
|
WindowMode: CompositionSettings.Current.Export.DefaultWindowMode,
|
|
ConfigData: CoreSettings.Config,
|
|
Author: symbol.SymbolPackage.AssemblyInformation?.Name ?? string.Empty, // todo - actual author name
|
|
BuildId: Guid.NewGuid(),
|
|
EditorVersion: Program.VersionText);
|
|
|
|
const string exportSettingsFile = "exportSettings.json";
|
|
if (JsonUtils.TrySaveJson(exportSettings, Path.Combine(exportDir, exportSettingsFile)))
|
|
return true;
|
|
|
|
reason = $"Failed to save export settings to {exportSettingsFile}";
|
|
return false;
|
|
}
|
|
|
|
private static bool TryRemoveExistingExportDir(out string reason, string exportDir)
|
|
{
|
|
try
|
|
{
|
|
if (Directory.Exists(exportDir))
|
|
{
|
|
Directory.Delete(exportDir, recursive: true);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
reason = $"Failed to remove export dir: {exportDir} ({e.Message}). Please close all files and File Explorer windows.";
|
|
return false;
|
|
}
|
|
|
|
reason = string.Empty;
|
|
return true;
|
|
}
|
|
|
|
private static string[] GetUnusedMappedDependencyFiles(ExportData exportData)
|
|
{
|
|
var requiredFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
var allMappedFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var definition in _dependencyDefinitions)
|
|
{
|
|
foreach (var filename in definition.Filenames)
|
|
{
|
|
allMappedFiles.Add(filename);
|
|
}
|
|
|
|
var isRequired = false;
|
|
foreach (var symbolId in definition.SymbolIds)
|
|
{
|
|
if (!exportData.ContainsSymbolId(symbolId))
|
|
continue;
|
|
|
|
isRequired = true;
|
|
break;
|
|
}
|
|
|
|
if (!isRequired)
|
|
continue;
|
|
|
|
foreach (var filename in definition.Filenames)
|
|
{
|
|
requiredFiles.Add(filename);
|
|
}
|
|
}
|
|
|
|
allMappedFiles.ExceptWith(requiredFiles);
|
|
return allMappedFiles.ToArray();
|
|
}
|
|
|
|
private sealed record OpDependencyDefinition(List<Guid> SymbolIds, List<string> Filenames);
|
|
|
|
private static readonly List<OpDependencyDefinition> _dependencyDefinitions =
|
|
[
|
|
new([new Guid("31ab98ec-5e79-4667-9a85-2fb168f41fa1")], [
|
|
"AbletonLink.deps.json",
|
|
"AbletonLinkDLL.dll",
|
|
"AbletonLink.dll",
|
|
]),
|
|
// Things related to webcams and MediaPipe
|
|
new([
|
|
new Guid("cd5a182e-254b-4e65-820b-ff754122614c"), // VideoDeviceInput
|
|
new Guid("A1B2C3D4-E5F6-4798-89AB-CDEF12345679"), // MediaPipeFaceDetection
|
|
new Guid("9b2c3d4e-5f6a-4798-89ab-cdef12345678"), // FaceLandmark
|
|
new Guid("4567890a-bcde-f123-4567-890abcdef123"), // HandLandmark
|
|
new Guid("23456789-0abc-def1-2345-67890abcdef1"), // Image Segmentation
|
|
new Guid("12345678-90ab-cdef-1234-567890abcdef"), // Object Detection
|
|
new Guid("34567890-abcd-ef12-3456-7890abcdef12"), // PoseLandmarkDetection
|
|
new Guid("8b23c93b-3b45-4c9b-9c23-4d5e6f7a8b9c"), // OnvifCamera
|
|
new Guid("7b4d3c2a-5b16-4b2a-8f3a-7e8c9d0b1a2b"), // CameraCalibrartor
|
|
// VideoStreamInput decodes via FFmpeg (the LGPL build shipped with the Video operator package, to
|
|
// which it has now moved), so it no longer needs the OpenCV DLLs.
|
|
], [
|
|
"mediapipe_c.dll",
|
|
"Emgu.CV.dll",
|
|
"Processing.NDI.Lib.x64.dll",
|
|
"OpenGL.Net.dll",
|
|
"OpenCvSharp.Extensions.dll",
|
|
"OpenCvSharpExtern.dll",
|
|
"opencv_videoio_ffmpeg4110_64.dll",
|
|
"DirectShowLib.dll",
|
|
"cvextern.dll",
|
|
]),
|
|
|
|
// NDI
|
|
new([
|
|
new Guid("7567c3b0-9d91-40d2-899d-3a95b481d023"), // NdiInput
|
|
new Guid("9412d0f4-dab8-4145-9719-10395e154fa7"), // NdiOutput
|
|
], [
|
|
"NDILibDotNet6.dll",
|
|
]),
|
|
|
|
// Unsplash
|
|
new([
|
|
new Guid("89162b9f-75f5-4d32-9d28-8259cf47cf58"),
|
|
], [
|
|
"Unsplasharp.dll",
|
|
]),
|
|
|
|
|
|
new([
|
|
new Guid("fc03dcd0-6f2f-4507-be06-1ed105607489"), //ArtnetInput
|
|
new Guid("98efc7c8-cafd-45ee-8746-14f37e9f59f8"), //ArtnetOutput
|
|
new Guid("faa3e182-96e6-45e7-b037-fb2acd88825b"), //ArtnetPixelOutput
|
|
], [
|
|
"ArtNet.dll",
|
|
]),
|
|
];
|
|
}
|
|
|