From 1c85dbfc3aea1df1d9bfbe7b67d6814a266f5ac4 Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Tue, 10 Oct 2017 22:24:02 +0200 Subject: [PATCH 01/33] Added ReSharper C# settings --- rhubarb/resharper.DotSettings | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/rhubarb/resharper.DotSettings b/rhubarb/resharper.DotSettings index af81b83..b16b555 100644 --- a/rhubarb/resharper.DotSettings +++ b/rhubarb/resharper.DotSettings @@ -1,5 +1,7 @@  ERROR + DO_NOT_SHOW + USE_TABS_ONLY False False False @@ -25,6 +27,25 @@ False END_OF_LINE CHOP_ALWAYS + END_OF_LINE + END_OF_LINE + False + END_OF_LINE + END_OF_LINE + TOGETHER + Tab + END_OF_LINE + END_OF_LINE + END_OF_LINE + False + False + False + END_OF_LINE + False + True + False + UseExplicitType + UseVarWhenEvident <NamingElement Priority="10"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="class field" /><type Name="struct field" /></Descriptor><Policy Inspect="True" Prefix="" Suffix="_" Style="aaBb" /></NamingElement> <NamingElement Priority="9"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="member function" /></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></NamingElement> <NamingElement Priority="11"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="PUBLIC"><type Name="class field" /><type Name="struct field" /></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></NamingElement> @@ -42,6 +63,7 @@ <NamingElement Priority="17"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="type alias" /><type Name="typedef" /></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></NamingElement> <NamingElement Priority="12"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="union member" /></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></NamingElement> <NamingElement Priority="3"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="union" /></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></NamingElement> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> @@ -83,6 +105,10 @@ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + C:\Users\Daniel\AppData\Local\JetBrains\Transient\ReSharperPlatformVs14\v09\SolutionCaches True True + True + True + True \ No newline at end of file From 7e7acb8068402c03931014ae5fe4dedb51ddcc1c Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Fri, 10 Nov 2017 20:52:02 +0100 Subject: [PATCH 02/33] Updated .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d047f52..717f29f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea/ .vs/ build/ +*.user \ No newline at end of file From df783f9afa7142422ac543b8e72e710a8bcc0e74 Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Fri, 10 Nov 2017 22:09:21 +0100 Subject: [PATCH 03/33] First working version of Rhubarb Lip Sync for Spine (C#, Windows Forms) --- .gitattributes | 63 +++ extras/rhubarb-for-spine/.gitignore | 3 + .../rhubarb-for-spine/rhubarb-for-spine.sln | 22 + .../rhubarb-for-spine/AnimationFileModel.cs | 147 +++++ .../rhubarb-for-spine/AudioFileModel.cs | 136 +++++ .../rhubarb-for-spine/BindableComboBox.cs | 30 + .../rhubarb-for-spine/MainForm.Designer.cs | 366 ++++++++++++ .../rhubarb-for-spine/MainForm.cs | 107 ++++ .../rhubarb-for-spine/MainForm.resx | 521 ++++++++++++++++++ .../rhubarb-for-spine/MainModel.cs | 50 ++ .../rhubarb-for-spine/ModelBase.cs | 24 + .../rhubarb-for-spine/MouthCue.cs | 17 + .../rhubarb-for-spine/MouthNaming.cs | 60 ++ .../rhubarb-for-spine/MouthShape.cs | 24 + .../rhubarb-for-spine/ProcessTools.cs | 70 +++ .../rhubarb-for-spine/Program.cs | 16 + .../Properties/AssemblyInfo.cs | 36 ++ .../DataSources/MainModel.datasource | 10 + .../Properties/Settings.Designer.cs | 26 + .../Properties/Settings.settings | 7 + .../rhubarb-for-spine/RhubarbCli.cs | 120 ++++ .../rhubarb-for-spine/SpineJson.cs | 156 ++++++ .../rhubarb-for-spine/Tools.cs | 66 +++ .../rhubarb-for-spine/app.config | 3 + .../rhubarb-for-spine/packages.config | 8 + .../rhubarb-for-spine.csproj | 138 +++++ .../rhubarb-for-spine/rhubarb.ico | Bin 0 -> 21947 bytes 27 files changed, 2226 insertions(+) create mode 100644 .gitattributes create mode 100644 extras/rhubarb-for-spine/.gitignore create mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine.sln create mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/AnimationFileModel.cs create mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/AudioFileModel.cs create mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/BindableComboBox.cs create mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/MainForm.Designer.cs create mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/MainForm.cs create mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/MainForm.resx create mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/MainModel.cs create mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/ModelBase.cs create mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/MouthCue.cs create mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/MouthNaming.cs create mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/MouthShape.cs create mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/ProcessTools.cs create mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/Program.cs create mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/Properties/AssemblyInfo.cs create mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/Properties/DataSources/MainModel.datasource create mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/Properties/Settings.Designer.cs create mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/Properties/Settings.settings create mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/RhubarbCli.cs create mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/SpineJson.cs create mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/Tools.cs create mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/app.config create mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/packages.config create mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/rhubarb-for-spine.csproj create mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/rhubarb.ico diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/extras/rhubarb-for-spine/.gitignore b/extras/rhubarb-for-spine/.gitignore new file mode 100644 index 0000000..cf7bd3f --- /dev/null +++ b/extras/rhubarb-for-spine/.gitignore @@ -0,0 +1,3 @@ +bin/ +obj/ +packages/ \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine.sln b/extras/rhubarb-for-spine/rhubarb-for-spine.sln new file mode 100644 index 0000000..6d9b2c3 --- /dev/null +++ b/extras/rhubarb-for-spine/rhubarb-for-spine.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.25420.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "rhubarb-for-spine", "rhubarb-for-spine\rhubarb-for-spine.csproj", "{C5ED6F8A-6141-4BAE-BE24-77DEE23E495F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C5ED6F8A-6141-4BAE-BE24-77DEE23E495F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C5ED6F8A-6141-4BAE-BE24-77DEE23E495F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C5ED6F8A-6141-4BAE-BE24-77DEE23E495F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C5ED6F8A-6141-4BAE-BE24-77DEE23E495F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/AnimationFileModel.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/AnimationFileModel.cs new file mode 100644 index 0000000..929f48c --- /dev/null +++ b/extras/rhubarb-for-spine/rhubarb-for-spine/AnimationFileModel.cs @@ -0,0 +1,147 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace rhubarb_for_spine { + public class AnimationFileModel : ModelBase { + private readonly string animationFilePath; + private readonly SemaphoreSlim semaphore; + private readonly JObject json; + + private string _mouthSlot; + private MouthNaming _mouthNaming; + private IReadOnlyCollection _mouthShapes; + private string _mouthShapesDisplayString; + private BindingList _audioFileModels; + + public AnimationFileModel(string animationFilePath, SemaphoreSlim semaphore) { + this.animationFilePath = animationFilePath; + this.semaphore = semaphore; + json = SpineJson.ReadJson(animationFilePath); + SpineJson.ValidateJson(json, AnimationFileDirectory); + + Slots = SpineJson.GetSlots(json); + MouthSlot = SpineJson.GuessMouthSlot(Slots); + var audioFileModels = SpineJson.GetAudioEvents(json) + .Select(CreateAudioFileModel) + .ToList(); + AudioFileModels = new BindingList(audioFileModels); + } + + public IReadOnlyCollection Slots { get; } + + public string MouthSlot { + get { return _mouthSlot; } + set { + _mouthSlot = value; + MouthNaming = GetMouthNaming(); + MouthShapes = GetMouthShapes(); + OnPropertyChanged(nameof(MouthSlot)); + } + } + + public MouthNaming MouthNaming { + get { return _mouthNaming; } + private set { + _mouthNaming = value; + OnPropertyChanged(nameof(MouthNaming)); + } + } + + public IReadOnlyCollection MouthShapes { + get { return _mouthShapes; } + private set { + _mouthShapes = value; + MouthShapesDisplayString = GetMouthShapesDisplayString(); + OnPropertyChanged(nameof(MouthShapes)); + } + } + + public string MouthShapesDisplayString { + get { return _mouthShapesDisplayString; } + set { + _mouthShapesDisplayString = value; + SetError(nameof(MouthShapesDisplayString), GetMouthShapesDisplayStringError()); + OnPropertyChanged(nameof(MouthShapesDisplayString)); + } + } + + public BindingList AudioFileModels { + get { return _audioFileModels; } + set { + _audioFileModels = value; + if (!_audioFileModels.Any()) { + SetError(nameof(AudioFileModels), "The JSON file doesn't contain any audio events."); + } + OnPropertyChanged(nameof(AudioFileModels)); + } + } + + private string AnimationFileDirectory => Path.GetDirectoryName(animationFilePath); + + private MouthNaming GetMouthNaming() { + IReadOnlyCollection mouthNames = SpineJson.GetSlotAttachmentNames(json, _mouthSlot); + return MouthNaming.Guess(mouthNames); + } + + private List GetMouthShapes() { + IReadOnlyCollection mouthNames = SpineJson.GetSlotAttachmentNames(json, _mouthSlot); + return rhubarb_for_spine.MouthShapes.All + .Where(shape => mouthNames.Contains(MouthNaming.GetName(shape))) + .ToList(); + } + + private string GetMouthShapesDisplayString() { + return MouthShapes + .Select(shape => shape.ToString()) + .Join(", "); + } + + private string GetMouthShapesDisplayStringError() { + var basicMouthShapes = rhubarb_for_spine.MouthShapes.Basic; + var missingBasicShapes = basicMouthShapes + .Where(shape => !MouthShapes.Contains(shape)) + .ToList(); + if (!missingBasicShapes.Any()) return null; + + var result = new StringBuilder(); + string missingString = string.Join(", ", missingBasicShapes); + result.AppendLine(missingBasicShapes.Count > 1 + ? $"Mouth shapes {missingString} are missing." + : $"Mouth shape {missingString} is missing."); + MouthShape first = basicMouthShapes.First(); + MouthShape last = basicMouthShapes.Last(); + result.Append($"At least the basic mouth shapes {first}-{last} need corresponding image attachments."); + return result.ToString(); + } + + private AudioFileModel CreateAudioFileModel(SpineJson.AudioEvent audioEvent) { + string audioDirectory = SpineJson.GetAudioDirectory(json, AnimationFileDirectory); + string filePath = Path.Combine(audioDirectory, audioEvent.RelativeAudioFilePath); + bool animatedPreviously = SpineJson.HasAnimation(json, GetAnimationName(audioEvent.Name)); + var extendedMouthShapes = MouthShapes.Where(shape => !rhubarb_for_spine.MouthShapes.IsBasic(shape)); + return new AudioFileModel( + audioEvent.Name, filePath, audioEvent.RelativeAudioFilePath, audioEvent.Dialog, + new HashSet(extendedMouthShapes), animatedPreviously, + semaphore, cues => AcceptAnimationResult(cues, audioEvent)); + } + + private string GetAnimationName(string audioEventName) { + return $"say_{audioEventName}"; + } + + private void AcceptAnimationResult( + IReadOnlyCollection mouthCues, SpineJson.AudioEvent audioEvent + ) { + string animationName = GetAnimationName(audioEvent.Name); + SpineJson.CreateOrUpdateAnimation( + json, mouthCues, audioEvent.Name, animationName, MouthSlot, MouthNaming); + File.WriteAllText(animationFilePath, json.ToString(Formatting.Indented)); + } + } +} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/AudioFileModel.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/AudioFileModel.cs new file mode 100644 index 0000000..dff3f26 --- /dev/null +++ b/extras/rhubarb-for-spine/rhubarb-for-spine/AudioFileModel.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Threading; + +namespace rhubarb_for_spine { + public class AudioFileModel : ModelBase { + private readonly string filePath; + private readonly ISet extendedMouthShapes; + private readonly SemaphoreSlim semaphore; + private readonly Action> reportResult; + private bool animatedPreviously; + private CancellationTokenSource cancellationTokenSource; + + private AudioFileStatus _status; + private string _actionLabel; + private double? _animationProgress; + + public AudioFileModel( + string name, + string filePath, + string displayFilePath, + string dialog, + ISet extendedMouthShapes, + bool animatedPreviously, + SemaphoreSlim semaphore, + Action> reportResult + ) { + this.reportResult = reportResult; + Name = name; + this.filePath = filePath; + this.extendedMouthShapes = extendedMouthShapes; + this.animatedPreviously = animatedPreviously; + this.semaphore = semaphore; + DisplayFilePath = displayFilePath; + Dialog = dialog; + Status = animatedPreviously ? AudioFileStatus.Done : AudioFileStatus.NotAnimated; + } + + public string Name { get; } + public string DisplayFilePath { get; } + public string Dialog { get; } + + public double? AnimationProgress { + get { return _animationProgress; } + private set { + _animationProgress = value; + OnPropertyChanged(nameof(AnimationProgress)); + + // Hack, so that a binding to Status will refresh + OnPropertyChanged(nameof(Status)); + } + } + + public AudioFileStatus Status { + get { return _status; } + private set { + _status = value; + ActionLabel = _status == AudioFileStatus.Scheduled || _status == AudioFileStatus.Animating + ? "Cancel" + : "Animate"; + OnPropertyChanged(nameof(Status)); + } + } + + public string ActionLabel { + get { return _actionLabel; } + private set { + _actionLabel = value; + OnPropertyChanged(nameof(ActionLabel)); + } + } + + public void PerformAction() { + switch (Status) { + case AudioFileStatus.NotAnimated: + case AudioFileStatus.Done: + StartAnimation(); + break; + case AudioFileStatus.Scheduled: + case AudioFileStatus.Animating: + CancelAnimation(); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + private async void StartAnimation() { + cancellationTokenSource = new CancellationTokenSource(); + Status = AudioFileStatus.Scheduled; + try { + await semaphore.WaitAsync(cancellationTokenSource.Token); + Status = AudioFileStatus.Animating; + try { + var progress = new Progress(value => AnimationProgress = value); + IReadOnlyCollection mouthCues = await RhubarbCli.AnimateAsync( + filePath, Dialog, extendedMouthShapes, progress, cancellationTokenSource.Token); + animatedPreviously = true; + Status = AudioFileStatus.Done; + reportResult(mouthCues); + } finally { + AnimationProgress = null; + semaphore.Release(); + cancellationTokenSource = null; + } + } catch (OperationCanceledException) { + Status = animatedPreviously ? AudioFileStatus.Done : AudioFileStatus.NotAnimated; + } + } + + private void CancelAnimation() { + cancellationTokenSource.Cancel(); + } + + private class Progress : IProgress { + private readonly Action report; + private double value; + + public Progress(Action report) { + this.report = report; + } + + public void Report(double progress) { + value = progress; + report(progress); + } + } + } + + public enum AudioFileStatus { + NotAnimated, + Scheduled, + Animating, + Done + } +} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/BindableComboBox.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/BindableComboBox.cs new file mode 100644 index 0000000..c619ed5 --- /dev/null +++ b/extras/rhubarb-for-spine/rhubarb-for-spine/BindableComboBox.cs @@ -0,0 +1,30 @@ +using System; +using System.Linq; +using System.Windows.Forms; + +namespace rhubarb_for_spine { + /// + /// A modification of the standard in which a data binding + /// on the SelectedItem property with the update mode set to DataSourceUpdateMode.OnPropertyChanged + /// actually updates when a selection is made in the combobox. + /// Code taken from https://stackoverflow.com/a/8392100/52041 + /// + public class BindableComboBox : ComboBox { + /// + protected override void OnSelectionChangeCommitted(EventArgs e) { + base.OnSelectionChangeCommitted(e); + + var bindings = DataBindings + .Cast() + .Where(binding => binding.PropertyName == nameof(SelectedItem) + && binding.DataSourceUpdateMode == DataSourceUpdateMode.OnPropertyChanged); + foreach (Binding binding in bindings) { + // Force the binding to update from the new SelectedItem + binding.WriteValue(); + + // Force the Textbox to update from the binding + binding.ReadValue(); + } + } + } +} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/MainForm.Designer.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/MainForm.Designer.cs new file mode 100644 index 0000000..20e4089 --- /dev/null +++ b/extras/rhubarb-for-spine/rhubarb-for-spine/MainForm.Designer.cs @@ -0,0 +1,366 @@ +using System; + +namespace rhubarb_for_spine { + partial class MainForm { + + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) { + if (disposing && (components != null)) { + components.Dispose(); + } + base.Dispose(disposing); + } + + protected override void OnLoad(EventArgs e) { + base.OnLoad(e); + + // Some bindings cannot be added in InitializeComponent; see https://stackoverflow.com/a/47167781/52041. + mouthSlotComboBox.DataBindings.Add(new System.Windows.Forms.Binding("DataSource", animationFileBindingSource, "Slots", true)); + mouthSlotComboBox.DataBindings.Add(new System.Windows.Forms.Binding("SelectedItem", animationFileBindingSource, "MouthSlot", true, System.Windows.Forms.DataSourceUpdateMode.OnPropertyChanged)); + audioEventsDataGridView.AutoGenerateColumns = false; + audioEventsDataGridView.DataBindings.Add(new System.Windows.Forms.Binding("DataSource", animationFileBindingSource, "AudioFileModels", true)); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() { + this.components = new System.ComponentModel.Container(); + System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle2 = new System.Windows.Forms.DataGridViewCellStyle(); + System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle(); + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm)); + this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); + this.filePathLabel = new System.Windows.Forms.Label(); + this.panel1 = new System.Windows.Forms.Panel(); + this.filePathTextBox = new System.Windows.Forms.TextBox(); + this.filePathBrowseButton = new System.Windows.Forms.Button(); + this.mouthSlotLabel = new System.Windows.Forms.Label(); + this.animationFileBindingSource = new System.Windows.Forms.BindingSource(this.components); + this.mouthShapesLabel = new System.Windows.Forms.Label(); + this.audioEventsLabel = new System.Windows.Forms.Label(); + this.audioEventsDataGridView = new System.Windows.Forms.DataGridView(); + this.mouthShapesResultLabel = new System.Windows.Forms.Label(); + this.mouthNamingLabel = new System.Windows.Forms.Label(); + this.mouthNamingResultLabel = new System.Windows.Forms.Label(); + this.mainErrorProvider = new System.Windows.Forms.ErrorProvider(this.components); + this.animationFileErrorProvider = new System.Windows.Forms.ErrorProvider(this.components); + this.mainBindingSource = new System.Windows.Forms.BindingSource(this.components); + this.mouthSlotComboBox = new rhubarb_for_spine.BindableComboBox(); + this.eventColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.audioFileColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.dialogColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.statusColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.actionColumn = new System.Windows.Forms.DataGridViewButtonColumn(); + this.tableLayoutPanel1.SuspendLayout(); + this.panel1.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.animationFileBindingSource)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.audioEventsDataGridView)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.mainErrorProvider)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.animationFileErrorProvider)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.mainBindingSource)).BeginInit(); + this.SuspendLayout(); + // + // tableLayoutPanel1 + // + this.tableLayoutPanel1.AutoSize = true; + this.tableLayoutPanel1.ColumnCount = 2; + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayoutPanel1.Controls.Add(this.filePathLabel, 0, 0); + this.tableLayoutPanel1.Controls.Add(this.panel1, 1, 0); + this.tableLayoutPanel1.Controls.Add(this.mouthSlotLabel, 0, 1); + this.tableLayoutPanel1.Controls.Add(this.mouthSlotComboBox, 1, 1); + this.tableLayoutPanel1.Controls.Add(this.mouthShapesLabel, 0, 3); + this.tableLayoutPanel1.Controls.Add(this.audioEventsLabel, 0, 4); + this.tableLayoutPanel1.Controls.Add(this.audioEventsDataGridView, 1, 4); + this.tableLayoutPanel1.Controls.Add(this.mouthShapesResultLabel, 1, 3); + this.tableLayoutPanel1.Controls.Add(this.mouthNamingLabel, 0, 2); + this.tableLayoutPanel1.Controls.Add(this.mouthNamingResultLabel, 1, 2); + this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill; + this.tableLayoutPanel1.Location = new System.Drawing.Point(5, 5); + this.tableLayoutPanel1.Name = "tableLayoutPanel1"; + this.tableLayoutPanel1.Padding = new System.Windows.Forms.Padding(3); + this.tableLayoutPanel1.RowCount = 5; + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 20F)); + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 20F)); + this.tableLayoutPanel1.Size = new System.Drawing.Size(903, 612); + this.tableLayoutPanel1.TabIndex = 0; + // + // filePathLabel + // + this.filePathLabel.AutoSize = true; + this.filePathLabel.Location = new System.Drawing.Point(6, 3); + this.filePathLabel.Name = "filePathLabel"; + this.filePathLabel.Padding = new System.Windows.Forms.Padding(0, 6, 0, 0); + this.filePathLabel.Size = new System.Drawing.Size(81, 19); + this.filePathLabel.TabIndex = 0; + this.filePathLabel.Text = "Spine JSON file"; + // + // panel1 + // + this.panel1.AutoSize = true; + this.panel1.Controls.Add(this.filePathTextBox); + this.panel1.Controls.Add(this.filePathBrowseButton); + this.panel1.Dock = System.Windows.Forms.DockStyle.Top; + this.panel1.Location = new System.Drawing.Point(93, 6); + this.panel1.Name = "panel1"; + this.panel1.Size = new System.Drawing.Size(804, 23); + this.panel1.TabIndex = 1; + // + // filePathTextBox + // + this.filePathTextBox.DataBindings.Add(new System.Windows.Forms.Binding("Text", this.mainBindingSource, "FilePath", true, System.Windows.Forms.DataSourceUpdateMode.OnPropertyChanged)); + this.mainErrorProvider.SetIconPadding(this.filePathTextBox, -20); + this.filePathTextBox.Location = new System.Drawing.Point(0, 0); + this.filePathTextBox.Name = "filePathTextBox"; + this.filePathTextBox.Size = new System.Drawing.Size(769, 20); + this.filePathTextBox.TabIndex = 2; + // + // filePathBrowseButton + // + this.filePathBrowseButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.filePathBrowseButton.Location = new System.Drawing.Point(775, 0); + this.filePathBrowseButton.Name = "filePathBrowseButton"; + this.filePathBrowseButton.Size = new System.Drawing.Size(30, 20); + this.filePathBrowseButton.TabIndex = 3; + this.filePathBrowseButton.Text = "..."; + this.filePathBrowseButton.UseVisualStyleBackColor = true; + this.filePathBrowseButton.Click += new System.EventHandler(this.filePathBrowseButton_Click); + // + // mouthSlotLabel + // + this.mouthSlotLabel.AutoSize = true; + this.mouthSlotLabel.Location = new System.Drawing.Point(6, 32); + this.mouthSlotLabel.Name = "mouthSlotLabel"; + this.mouthSlotLabel.Padding = new System.Windows.Forms.Padding(0, 6, 0, 0); + this.mouthSlotLabel.Size = new System.Drawing.Size(56, 19); + this.mouthSlotLabel.TabIndex = 2; + this.mouthSlotLabel.Text = "Mouth slot"; + // + // animationFileBindingSource + // + this.animationFileBindingSource.DataMember = "AnimationFileModel"; + this.animationFileBindingSource.DataSource = this.mainBindingSource; + // + // mouthShapesLabel + // + this.mouthShapesLabel.AutoSize = true; + this.mouthShapesLabel.Location = new System.Drawing.Point(6, 79); + this.mouthShapesLabel.Name = "mouthShapesLabel"; + this.mouthShapesLabel.Padding = new System.Windows.Forms.Padding(0, 6, 0, 0); + this.mouthShapesLabel.Size = new System.Drawing.Size(74, 19); + this.mouthShapesLabel.TabIndex = 4; + this.mouthShapesLabel.Text = "Mouth shapes"; + // + // audioEventsLabel + // + this.audioEventsLabel.AutoSize = true; + this.audioEventsLabel.Location = new System.Drawing.Point(6, 98); + this.audioEventsLabel.Name = "audioEventsLabel"; + this.audioEventsLabel.Padding = new System.Windows.Forms.Padding(0, 6, 0, 0); + this.audioEventsLabel.Size = new System.Drawing.Size(69, 19); + this.audioEventsLabel.TabIndex = 6; + this.audioEventsLabel.Text = "Audio events"; + // + // audioEventsDataGridView + // + this.audioEventsDataGridView.AutoSizeRowsMode = System.Windows.Forms.DataGridViewAutoSizeRowsMode.AllCellsExceptHeaders; + this.audioEventsDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; + this.audioEventsDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { + this.eventColumn, + this.audioFileColumn, + this.dialogColumn, + this.statusColumn, + this.actionColumn}); + dataGridViewCellStyle2.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft; + dataGridViewCellStyle2.BackColor = System.Drawing.SystemColors.Window; + dataGridViewCellStyle2.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + dataGridViewCellStyle2.ForeColor = System.Drawing.SystemColors.ControlText; + dataGridViewCellStyle2.SelectionBackColor = System.Drawing.SystemColors.Window; + dataGridViewCellStyle2.SelectionForeColor = System.Drawing.SystemColors.ControlText; + dataGridViewCellStyle2.WrapMode = System.Windows.Forms.DataGridViewTriState.False; + this.audioEventsDataGridView.DefaultCellStyle = dataGridViewCellStyle2; + this.audioEventsDataGridView.Dock = System.Windows.Forms.DockStyle.Fill; + this.animationFileErrorProvider.SetIconPadding(this.audioEventsDataGridView, -20); + this.audioEventsDataGridView.Location = new System.Drawing.Point(93, 101); + this.audioEventsDataGridView.Name = "audioEventsDataGridView"; + this.audioEventsDataGridView.ReadOnly = true; + this.audioEventsDataGridView.RowHeadersVisible = false; + this.audioEventsDataGridView.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect; + this.audioEventsDataGridView.Size = new System.Drawing.Size(804, 505); + this.audioEventsDataGridView.TabIndex = 7; + this.audioEventsDataGridView.CellClick += new System.Windows.Forms.DataGridViewCellEventHandler(this.audioEventsDataGridView_CellClick); + this.audioEventsDataGridView.CellPainting += new System.Windows.Forms.DataGridViewCellPaintingEventHandler(this.audioEventsDataGridView_CellPainting); + // + // mouthShapesResultLabel + // + this.mouthShapesResultLabel.AutoSize = true; + this.mouthShapesResultLabel.DataBindings.Add(new System.Windows.Forms.Binding("Text", this.animationFileBindingSource, "MouthShapesDisplayString", true)); + this.mouthShapesResultLabel.Location = new System.Drawing.Point(93, 79); + this.mouthShapesResultLabel.Name = "mouthShapesResultLabel"; + this.mouthShapesResultLabel.Padding = new System.Windows.Forms.Padding(0, 6, 0, 0); + this.mouthShapesResultLabel.Size = new System.Drawing.Size(16, 19); + this.mouthShapesResultLabel.TabIndex = 8; + this.mouthShapesResultLabel.Text = " "; + // + // mouthNamingLabel + // + this.mouthNamingLabel.AutoSize = true; + this.mouthNamingLabel.Location = new System.Drawing.Point(6, 59); + this.mouthNamingLabel.Name = "mouthNamingLabel"; + this.mouthNamingLabel.Padding = new System.Windows.Forms.Padding(0, 6, 0, 0); + this.mouthNamingLabel.Size = new System.Drawing.Size(74, 19); + this.mouthNamingLabel.TabIndex = 9; + this.mouthNamingLabel.Text = "Mouth naming"; + // + // mouthNamingResultLabel + // + this.mouthNamingResultLabel.AutoSize = true; + this.mouthNamingResultLabel.DataBindings.Add(new System.Windows.Forms.Binding("Text", this.animationFileBindingSource, "MouthNaming.DisplayString", true)); + this.mouthNamingResultLabel.Location = new System.Drawing.Point(93, 59); + this.mouthNamingResultLabel.Name = "mouthNamingResultLabel"; + this.mouthNamingResultLabel.Padding = new System.Windows.Forms.Padding(0, 6, 0, 0); + this.mouthNamingResultLabel.Size = new System.Drawing.Size(16, 19); + this.mouthNamingResultLabel.TabIndex = 10; + this.mouthNamingResultLabel.Text = " "; + // + // mainErrorProvider + // + this.mainErrorProvider.BlinkStyle = System.Windows.Forms.ErrorBlinkStyle.NeverBlink; + this.mainErrorProvider.ContainerControl = this; + this.mainErrorProvider.DataSource = this.mainBindingSource; + // + // animationFileErrorProvider + // + this.animationFileErrorProvider.BlinkStyle = System.Windows.Forms.ErrorBlinkStyle.NeverBlink; + this.animationFileErrorProvider.ContainerControl = this; + this.animationFileErrorProvider.DataSource = this.animationFileBindingSource; + // + // mainBindingSource + // + this.mainBindingSource.DataSource = typeof(rhubarb_for_spine.MainModel); + // + // mouthSlotComboBox + // + this.mouthSlotComboBox.Dock = System.Windows.Forms.DockStyle.Top; + this.mouthSlotComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.mouthSlotComboBox.Location = new System.Drawing.Point(93, 35); + this.mouthSlotComboBox.Name = "mouthSlotComboBox"; + this.mouthSlotComboBox.Size = new System.Drawing.Size(804, 21); + this.mouthSlotComboBox.TabIndex = 3; + // + // eventColumn + // + this.eventColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; + this.eventColumn.DataPropertyName = "Name"; + this.eventColumn.HeaderText = "Event"; + this.eventColumn.Name = "eventColumn"; + this.eventColumn.ReadOnly = true; + // + // audioFileColumn + // + this.audioFileColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; + this.audioFileColumn.DataPropertyName = "DisplayFilePath"; + this.audioFileColumn.HeaderText = "Audio file"; + this.audioFileColumn.Name = "audioFileColumn"; + this.audioFileColumn.ReadOnly = true; + // + // dialogColumn + // + this.dialogColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; + this.dialogColumn.DataPropertyName = "Dialog"; + dataGridViewCellStyle1.WrapMode = System.Windows.Forms.DataGridViewTriState.True; + this.dialogColumn.DefaultCellStyle = dataGridViewCellStyle1; + this.dialogColumn.FillWeight = 300F; + this.dialogColumn.HeaderText = "Dialog"; + this.dialogColumn.Name = "dialogColumn"; + this.dialogColumn.ReadOnly = true; + // + // statusColumn + // + this.statusColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; + this.statusColumn.DataPropertyName = "Status"; + this.statusColumn.HeaderText = "Status"; + this.statusColumn.Name = "statusColumn"; + this.statusColumn.ReadOnly = true; + // + // actionColumn + // + this.actionColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.None; + this.actionColumn.DataPropertyName = "ActionLabel"; + this.actionColumn.HeaderText = ""; + this.actionColumn.Name = "actionColumn"; + this.actionColumn.ReadOnly = true; + this.actionColumn.Resizable = System.Windows.Forms.DataGridViewTriState.True; + this.actionColumn.Text = ""; + this.actionColumn.Width = 80; + // + // MainForm + // + this.AllowDrop = true; + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(913, 622); + this.Controls.Add(this.tableLayoutPanel1); + this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); + this.Name = "MainForm"; + this.Padding = new System.Windows.Forms.Padding(5); + this.Text = "Rhubarb Lip Sync for Spine"; + this.DragDrop += new System.Windows.Forms.DragEventHandler(this.MainForm_DragDrop); + this.DragEnter += new System.Windows.Forms.DragEventHandler(this.MainForm_DragEnter); + this.tableLayoutPanel1.ResumeLayout(false); + this.tableLayoutPanel1.PerformLayout(); + this.panel1.ResumeLayout(false); + this.panel1.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)(this.animationFileBindingSource)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.audioEventsDataGridView)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.mainErrorProvider)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.animationFileErrorProvider)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.mainBindingSource)).EndInit(); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; + private System.Windows.Forms.Label filePathLabel; + private System.Windows.Forms.Panel panel1; + private System.Windows.Forms.TextBox filePathTextBox; + private System.Windows.Forms.Button filePathBrowseButton; + private System.Windows.Forms.Label mouthSlotLabel; + private BindableComboBox mouthSlotComboBox; + private System.Windows.Forms.Label mouthShapesLabel; + private System.Windows.Forms.Label audioEventsLabel; + private System.Windows.Forms.DataGridView audioEventsDataGridView; + private System.Windows.Forms.BindingSource mainBindingSource; + private System.Windows.Forms.ErrorProvider mainErrorProvider; + private System.Windows.Forms.Label mouthShapesResultLabel; + private System.Windows.Forms.Label mouthNamingLabel; + private System.Windows.Forms.Label mouthNamingResultLabel; + private System.Windows.Forms.BindingSource animationFileBindingSource; + private System.Windows.Forms.ErrorProvider animationFileErrorProvider; + private System.Windows.Forms.DataGridViewTextBoxColumn eventColumn; + private System.Windows.Forms.DataGridViewTextBoxColumn audioFileColumn; + private System.Windows.Forms.DataGridViewTextBoxColumn dialogColumn; + private System.Windows.Forms.DataGridViewTextBoxColumn statusColumn; + private System.Windows.Forms.DataGridViewButtonColumn actionColumn; + } +} + diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/MainForm.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/MainForm.cs new file mode 100644 index 0000000..80f3f5a --- /dev/null +++ b/extras/rhubarb-for-spine/rhubarb-for-spine/MainForm.cs @@ -0,0 +1,107 @@ +using System; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; + +namespace rhubarb_for_spine { + public partial class MainForm : Form { + private MainModel MainModel { get; } + + public MainForm(MainModel mainModel) { + InitializeComponent(); + MainModel = mainModel; + mainBindingSource.DataSource = mainModel; + } + + private void filePathBrowseButton_Click(object sender, EventArgs e) { + var openFileDialog = new OpenFileDialog { + Filter = "JSON files (*.json)|*.json", + InitialDirectory = filePathTextBox.Text + }; + if (openFileDialog.ShowDialog() == DialogResult.OK) { + filePathTextBox.Text = openFileDialog.FileName; + } + } + + private void MainForm_DragEnter(object sender, DragEventArgs e) { + if (e.Data.GetDataPresent(DataFormats.FileDrop)) { + e.Effect = DragDropEffects.Copy; + } + } + + private void MainForm_DragDrop(object sender, DragEventArgs e) { + var files = (string[]) e.Data.GetData(DataFormats.FileDrop); + if (files.Any()) { + filePathTextBox.Text = files.First(); + } + } + + private void audioEventsDataGridView_CellClick(object sender, DataGridViewCellEventArgs e) { + if (e.ColumnIndex != actionColumn.Index) return; + + AudioFileModel audioFileModel = GetAudioFileModel(e.RowIndex); + audioFileModel?.PerformAction(); + } + + private void audioEventsDataGridView_CellPainting(object sender, DataGridViewCellPaintingEventArgs e) { + if (e.ColumnIndex != statusColumn.Index || e.RowIndex == -1) return; + + e.PaintBackground(e.CellBounds, false); + AudioFileModel audioFileModel = GetAudioFileModel(e.RowIndex); + if (audioFileModel == null) return; + + string text = string.Empty; + StringAlignment horizontalTextAlignment = StringAlignment.Near; + Color backgroundColor = Color.Transparent; + double? progress = null; + switch (audioFileModel.Status) { + case AudioFileStatus.NotAnimated: + break; + case AudioFileStatus.Scheduled: + text = "Waiting"; + backgroundColor = SystemColors.Highlight.WithOpacity(0.2); + break; + case AudioFileStatus.Animating: + progress = audioFileModel.AnimationProgress ?? 0.0; + text = $"{(int) (progress * 100)}%"; + horizontalTextAlignment = StringAlignment.Center; + break; + case AudioFileStatus.Done: + text = "Done"; + break; + default: + throw new ArgumentOutOfRangeException(); + } + + // Draw background + var bounds = e.CellBounds; + e.Graphics.FillRectangle(new SolidBrush(backgroundColor), bounds); + + // Draw progress bar + if (progress.HasValue) { + e.Graphics.FillRectangle( + SystemBrushes.Highlight, + bounds.X, bounds.Y, bounds.Width * (float) progress, bounds.Height); + } + + // Draw text + var stringFormat = new StringFormat { + Alignment = horizontalTextAlignment, + LineAlignment = StringAlignment.Center + }; + e.Graphics.DrawString( + text, + e.CellStyle.Font, new SolidBrush(e.CellStyle.ForeColor), + bounds, stringFormat); + + e.Handled = true; + } + + private AudioFileModel GetAudioFileModel(int rowIndex) { + return rowIndex < 0 + ? null + : MainModel.AnimationFileModel?.AudioFileModels[rowIndex]; + } + } + +} diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/MainForm.resx b/extras/rhubarb-for-spine/rhubarb-for-spine/MainForm.resx new file mode 100644 index 0000000..52ef75b --- /dev/null +++ b/extras/rhubarb-for-spine/rhubarb-for-spine/MainForm.resx @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + + 209, 17 + + + 67 + + + + + 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 + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAASUpKEElKSkBJSkpASUpKQElKSkBJSkoQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAASUpKEElKSoBJSkrPSUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpKr0lKSmAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + 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 + Yf//lWH//5Vh//+VYf//lWH//5Vh//+VYf//glvS/5Vh//+VYf//lWH//5Vh//+VYf//lWH//5Vh//+M + Xuj/SUpK/0lKSt9JSkoQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASUpKv0lKSv+M + Xuj/lWH//5Vh//+VYf//lWH//5Vh//+VYf//lWH//5Vh//90V7D/glvS/5Vh//+VYf//lWH//5Vh//+V + Yf//lWH//5Vh//9hUYP/SUpK/0lKSv9JSkqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJ + SkpASUpK/0lKSv9XTmz/kGD0/5Vh//+VYf//lWH//5Vh//+VYf//lWH//3lYu/9hUYP/lWH//5Vh//+V + Yf//lWH//5Vh//+VYf//lWH//2ZTjv9JSkr/SUpK/0lKSv9JSkr/SUpKIAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAABJSkrfSUpK/0lKSv9JSkr/TktV/29Wpf+MXuj/lWH//5Vh//99Wsb/YVGD/0lKSv9h + UYP/jF7o/5Vh//+VYf//lWH//5Vh//+CW9L/XFB3/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpKvwAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAElKSmBJSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J + Skr/SUpK/0lKSv9JSkr/SUpK/1NNYf9cUHf/XFB3/05LVf9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J + Skr/SUpK/0lKSkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSr9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J + Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J + Skr/SUpK/0lKSv9JSkr/SUpK/0lKSq8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASUpKQElKSv9JSkr/SUpK/0lKSv9J + Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J + Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkogAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASUpKn0lKSv9J + Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J + Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkqPAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJ + SkoQSUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J + Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J + SkrfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAABJSkpgSUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J + Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J + Skr/SUpK/0lKSv9JSkr/SUpKUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkqvSUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J + Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J + Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpKnwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKShBJSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J + Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J + Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK7wAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSmBJSkr/SUpK/0lKSv9J + Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J + Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSjAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSp9J + Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/2tsbP+Njo7/pKWl/8bGxv/S + 0tL/u7u7/42Ojv/Gxsb/pKWl/42Ojv9rbGz/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J + Skr/SUpK/0lKSo8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAElKSu9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/VFVV/1RVVf/Gxsb/9PT0//////// + ////////////////////0tLS/6Slpf//////////////////////9PT0/7u7u/93d3f/SUpK/0lKSv9J + Skr/SUpK/0lKSv9JSkr/SUpK/0lKSt8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAASUpKMElKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/2BhYf+7u7v/9PT0/42Ojv// + ////////////////////////////////////0tLS/6Slpf////////////////////////////////// + ////mZmZ/3d3d/9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkogAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASUpKj0lKSv9JSkr/SUpK/0lKSv9JSkr/pKWl//////// + ////0tLS/7u7u///////////////////////////////////////0tLS/6Slpf////////////////// + ////////////////////0tLS/7u7u//S0tL/a2xs/0lKSv9JSkr/SUpK/0lKSv9JSkpwAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASUpKz0lKSv9JSkr/SUpK/2tsbP/o + 6Oj/////////////////pKWl/+jo6P//////////////////////////////////////0tLS/6Slpf// + /////////////////////////////////////////42Ojv///////////6Slpf9JSkr/SUpK/0lKSv9J + SkrPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkogSUpK/0lKSv9J + Skr/SUpK/9LS0v//////////////////////goOD///////S0tL/u7u7/6Slpf93d3f/d3d3/0lKSv9J + Skr/SUpK/0lKSv9JSkr/SUpK/3d3d/+Njo7/r7Cw/93d3f///////////42Ojv////////////////+v + sLD/SUpK/0lKSv9JSkr/SUpKIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJ + SkpwSUpK/0lKSv9JSkr/a2xs/////////////////93d3f+ZmZn/YGFh/0lKSv9JSkr/SUpK/0lKSv9J + Skq/SUpKr0lKSoBJSkqASUpKgElKSoBJSkqASUpKgElKSp9JSkq/SUpK/0lKSv9gYWH/mZmZ/4KDg//o + 6Oj/////////////////VFVV/0lKSv9JSkr/SUpKcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAABJSkrPSUpK/0lKSv9JSkr/u7u7/8bGxv+Njo7/VFVV/0lKSv9JSkr/SUpKz0lKSo9J + SkpgSUpKMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSkBJ + SkqPSUpK30lKSv9gYWH/u7u7////////////mZmZ/0lKSv9JSkr/SUpK3wAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAElKSkBJSkr/SUpK/0lKSv9JSkr/VFVV/0lKSv9JSkr/SUpKr0lKSmBJ + SkogAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAElKSlBJSkq/SUpK/2BhYf/Gxsb/3d3d/0lKSv9JSkr/SUpK/0lKSjAA + AAAAAAAAAAAAAAAAAAAAAAAAAElKSt9JSkpwAAAAAElKSp9JSkr/SUpK/0lKSv9JSkr/SUpKv0lKSmBJ + SkoQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASUpKQElKSr9JSkr/d3d3/1RVVf9J + Skr/SUpK/0lKSp8AAAAAAAAAAAAAAAAAAAAAAAAAAElKSjBJSkrPSUpK30lKSv9JSkr/SUpK/0lKSp9J + SkowAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJ + SkpQSUpK30lKSv9JSkr/SUpK/0lKSu9JSkoQAAAAAAAAAAAAAAAAAAAAAAAAAABJSkoQSUpKgElKSu9J + Skr/SUpK30lKSp9JSkqASUpKnwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAASUpKEElKSq9JSkr/SUpK/0lKSv9JSkpgAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAElKShBJSkpASUpKgElKSnBJSkpASUpKEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkpgSUpK70lKSv9JSkrfAAAAAAAAAABJ + SkpwSUpKrwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSiBJSkpASUpKcElKSu9J + Skr/SUpKr0lKSu9JSkrfSUpKMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSiBJ + SkqASUpKv0lKSr9JSkq/SUpKr0lKSmAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///////xVa////////FVr///////8VWv////// + /xVa///wP///FVr//4AP//8VWv//AgP//xVa//w/4P//FVr/+Ph4f/8VWv/xgAw//xVa//MAAj//FVr/ + 9gABn/8VWv/8AACf/xVa//gAAH//FVr/8AAAP/8VWv/gAAAf/xVa/+AAAB//FVr/wAAAD/8VWv/AAAAP + /xVa/4AAAAf/FVr/gAAAB/8VWv8AAAAD/xVa/wAAAAP/FVr+AAAAA/8VWv4AAAAB/xVa/gAAAAH/FVr8 + AAAAAf8VWvwAAAAA/xVa/AAAAAD/FVr8AAAAAP8VWvgAAAAAfxVa+AAAAAB/FVr4AAAAAH8VWvAAAAAA + PxVa8AAAAAA/FVrwAD/8AD8VWuAD//+AHxVaIB///+AfFVoA////+A8VWoB////8DxVa4H////8MFVr/ + /////gAVWv/////+AxVa////////FVr///////8VWv///////xVa////////FVr///////8VWigAAAAg + AAAAQAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkpgSUpKr0lKSr9J + Skr/SUpKv0lKSo9JSkowAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkpASUpK30lKSp9J + SkpgSUpKQElKSkBJSkpASUpKj0lKSt9JSkq/SUpKIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASUpKYElKSs9J + SkogAAAAAAAAAABJSkowSUpKQElKSiAAAAAAAAAAAElKSoBJSkrvSUpKQAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSlBJ + SkqfAAAAAElKSkBJSkqvSUpK/0lKSv9JSkr/SUpK/0lKSu9JSkqPSUpKEElKSjBJSkrfSUpKMAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAASUpKgAAAAABJSkqASUpK/1xQd/99Wsb/kGD0/5Vh//+MXuj/eVi7/1NNYf9JSkrvSUpKUElKSjBJ + SkrPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAASUpKn05LVf99Wsb/lWH//5Vh//+VYf//lWH//5Vh//+VYf//lWH//3RXsP9J + Skr/SUpKYElKSjBJSkogAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAElKSnBOS1X/h13d/5Vh//+VYf//lWH//5Vh//+VYf//lWH//5Vh//+V + Yf//lWH//3lYu/9JSkrvSUpKMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkogTktV/4dd3f+VYf//lWH//5Vh//+VYf//lWH//31axv+V + Yf//lWH//5Vh//+VYf//lWH//1NNYf9JSkrPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSr9OS1X/fVrG/5Vh//+VYf//lWH//5Vh//95 + WLv/dFew/5Vh//+VYf//lWH//5Vh//9mU47/SUpK/0lKSv9JSkpwAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkpASUpK/0lKSv9JSkr/YVGD/3lYu/90 + V7D/XFB3/0lKSv9mU47/glvS/4Jb0v95WLv/XFB3/0lKSv9JSkr/SUpK/0lKSu9JSkoQAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSr9JSkr/SUpK/0lKSv9J + Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSoAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkogSUpK/0lKSv9J + Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J + Skr/SUpK3wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSo9J + Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J + Skr/SUpK/0lKSv9JSkr/SUpKUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAASUpK30lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J + Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkqvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAElKSjBJSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J + Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAASUpKj0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv93 + d3f/VFVV/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSlAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkrPSUpK/0lKSv9JSkr/SUpK/0lKSv9UVVX/r7Cw/9LS0v// + //////////////+kpaX//////+jo6P+7u7v/jY6O/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpKnwAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASUpKIElKSv9JSkr/SUpK/0lKSv+Njo7/6Ojo/5mZmf// + /////////////////////////6Slpf//////////////////////xsbG/4KDg/9JSkr/SUpK/0lKSv9J + SkrvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkpwSUpK/0lKSv9rbGz/3d3d///////S + 0tL/xsbG///////////////////////S0tL/pKWl////////////////////////////pKWl/+jo6P9r + bGz/SUpK/0lKSv9JSkowAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSq9JSkr/SUpK/9LS0v// + /////////5mZmf+kpaX/jY6O/3d3d/9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv93d3f/mZmZ/93d3f+Z + mZn///////////9rbGz/SUpK/0lKSp8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkogSUpK/0lKSv9r + bGz/xsbG/42Ojv9UVVX/SUpK70lKSq9JSkqASUpKQElKSiAAAAAAAAAAAAAAAAAAAAAASUpKEElKSkBJ + SkqPSUpK30lKSv+vsLD/9PT0/6+wsP9JSkr/SUpK7wAAAAAAAAAAAAAAAAAAAABJSkpgAAAAAElKSoBJ + Skr/SUpK/0lKSv9JSkrfSUpKj0lKSkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAASUpKQElKSr9UVVX/pKWl/0lKSv9JSkr/SUpKUAAAAAAAAAAAAAAAAElKSmBJ + SkrfSUpK30lKSv9JSkq/SUpKUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSkBJSkrPSUpK/0lKSv9JSkqvAAAAAAAAAAAA + AAAAAAAAAElKSjBJSkqfSUpK30lKSs9JSkqASUpKEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkqASUpK/0lKSv9J + SkogAAAAAElKSkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSjBJ + SkqASUpK/0lKSr9JSkrfSUpKjwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAASUpKIElKSnBJSkqASUpKcElKSiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//////////// + //////Af///AB///jGP//yAB//9AAf//gAD//wAB//4AAf/+AAD//AAAf/wAAH/4AAB/+AAAP/gAAD/w + AAA/8AAAH/AAAB/gAAAf4AAAD+AAAA/AA8APQH/8BwP//weB///C////wP///8H///////////////8o + AAAAEAAAACAAAAABACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJ + SkqzSUpK/0lKSv9JSkr/SUpKswAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJ + SkqzAAAAAAAAAAAAAAAAAAAAAAAAAABJSkqzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAElKSv9JSkr/SUpK/0lKSv9JSkr/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAElKSv+AU6b/lWH//5Vh//+VYf//gFOm/0lKSv8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAElKSrOAU6b/lWH//5Vh//9/Uqb/lWH//5Vh//+AU6b/SUpKswAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAABJSkr/SUpK/4BTpv+AU6b/gFOm/4BTpv+AU6b/SUpK/0lKSv8AAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAABJSkqzSUpK/1lZWf9cXV3/WFlZ/1JTU/9cXV3/WFlZ/1tcXP9JSkr/SUpKswAAAAAA + AAAAAAAAAAAAAAAAAAAASUpK/0lKSv9mZ2f/amtr/29wcP90dXX/aGlp/21ubv9vcHD/SktL/0lKSv8A + AAAAAAAAAAAAAAAAAAAASUpKs0lKSv9JSkr/WFhY/1hYWP9KS0v/SktL/0pLS/9KS0v/SktL/1NUVP9J + Skr/AAAAAAAAAAAAAAAAAAAAAElKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J + Skr/SUpK/0lKSrMAAAAAAAAAAAAAAABJSkr/SUpK/0lKSv+jpKT///////////+jpKT///////////+j + pKT/SUpK/0lKSv9JSkr/AAAAAAAAAABJSkqzSUpK/0lKSv//////o6Sk////////////o6Sk//////// + ////o6Sk//////9JSkr/SUpK/wAAAABJSkqzSUpK/0lKSv//////o6Sk/6OkpP9JSkqzSUpK/0lKSv9J + Skr/SUpK/0lKSrOjpKT//////0lKSv9JSkqzSUpK/0lKSrNJSkr/o6Sk/0lKSv9JSkqzAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAASUpKs0lKSv+jpKT/SUpK/0lKSrMAAAAASUpK/0lKSv9JSkqzAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkqzSUpK/0lKSv8AAAAAAAAAAAAAAABJSkqzSUpK/0lKSrMA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkqzSUpK/0lKSrMAAAAAAAAAAPg/rEH336xB+D+sQfAfrEHg + D6xB4A+sQcAHrEHAB6xBgAesQYADrEGAA6xBAAKsQQAArEEH4KxBj/GsQcfjrEE= + + + + 359, 17 + + + True + + + True + + + True + + + True + + + True + + + 563, 17 + + \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/MainModel.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/MainModel.cs new file mode 100644 index 0000000..6c80a38 --- /dev/null +++ b/extras/rhubarb-for-spine/rhubarb-for-spine/MainModel.cs @@ -0,0 +1,50 @@ +using System; +using System.IO; +using System.Threading; + +namespace rhubarb_for_spine { + public class MainModel : ModelBase { + // For the time being, we're allowing only one file to be animated at a time. + // Rhubarb tries to use all processor cores, so there isn't much to be gained by allowing + // multiple parallel jobs. On the other hand, too many parallel Rhubarb instances may + // seriously slow down the system. + readonly SemaphoreSlim semaphore = new SemaphoreSlim(1); + + private string _filePath; + private AnimationFileModel _animationFileModel; + + public MainModel(string filePath = null) { + FilePath = filePath; + } + + public string FilePath { + get { return _filePath; } + set { + _filePath = value; + AnimationFileModel = null; + if (string.IsNullOrEmpty(_filePath)) { + SetError(nameof(FilePath), "No input file specified."); + } else if (!File.Exists(_filePath)) { + SetError(nameof(FilePath), "File does not exist."); + } else { + try { + AnimationFileModel = new AnimationFileModel(_filePath, semaphore); + SetError(nameof(FilePath), null); + } catch (Exception e) { + SetError(nameof(FilePath), e.Message); + } + } + OnPropertyChanged(nameof(FilePath)); + } + } + + public AnimationFileModel AnimationFileModel { + get { return _animationFileModel; } + set { + _animationFileModel = value; + OnPropertyChanged(nameof(AnimationFileModel)); + } + } + + } +} diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/ModelBase.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/ModelBase.cs new file mode 100644 index 0000000..98b30c4 --- /dev/null +++ b/extras/rhubarb-for-spine/rhubarb-for-spine/ModelBase.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.ComponentModel; + +namespace rhubarb_for_spine { + public class ModelBase : INotifyPropertyChanged, IDataErrorInfo { + private readonly Dictionary errors = new Dictionary(); + + public event PropertyChangedEventHandler PropertyChanged; + + protected void OnPropertyChanged(string propertyName) { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + protected void SetError(string propertyName, string error) { + errors[propertyName] = error; + } + + string IDataErrorInfo.this[string propertyName] => + errors.ContainsKey(propertyName) ? errors[propertyName] : null; + + string IDataErrorInfo.Error => null; + + } +} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/MouthCue.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/MouthCue.cs new file mode 100644 index 0000000..f0a062d --- /dev/null +++ b/extras/rhubarb-for-spine/rhubarb-for-spine/MouthCue.cs @@ -0,0 +1,17 @@ +using System; + +namespace rhubarb_for_spine { + public class MouthCue { + public MouthCue(TimeSpan time, MouthShape mouthShape) { + Time = time; + MouthShape = mouthShape; + } + + public TimeSpan Time { get; } + public MouthShape MouthShape { get; } + + public override string ToString() { + return $"{Time}: {MouthShape}"; + } + } +} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/MouthNaming.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/MouthNaming.cs new file mode 100644 index 0000000..bcffaac --- /dev/null +++ b/extras/rhubarb-for-spine/rhubarb-for-spine/MouthNaming.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Linq; + +namespace rhubarb_for_spine { + public class MouthNaming { + public MouthNaming(string prefix, string suffix, MouthShapeCasing mouthShapeCasing) { + Prefix = prefix; + Suffix = suffix; + MouthShapeCasing = mouthShapeCasing; + } + + public string Prefix { get; } + public string Suffix { get; } + public MouthShapeCasing MouthShapeCasing { get; } + + public static MouthNaming Guess(IReadOnlyCollection mouthNames) { + string firstMouthName = mouthNames.First(); + if (mouthNames.Count == 1) { + return firstMouthName == string.Empty + ? new MouthNaming(string.Empty, string.Empty, MouthShapeCasing.Lower) + : new MouthNaming( + firstMouthName.Substring(0, firstMouthName.Length - 1), + string.Empty, + GuessMouthShapeCasing(firstMouthName.Last())); + } + + string commonPrefix = mouthNames.GetCommonPrefix(); + string commonSuffix = mouthNames.GetCommonSuffix(); + var mouthShapeCasing = firstMouthName.Length > commonPrefix.Length + ? GuessMouthShapeCasing(firstMouthName[commonPrefix.Length]) + : MouthShapeCasing.Lower; + return new MouthNaming(commonPrefix, commonSuffix, mouthShapeCasing); + } + + public string DisplayString { + get { + string casing = MouthShapeCasing == MouthShapeCasing.Upper + ? "" + : ""; + return $"\"{Prefix}{casing}{Suffix}\""; + } + } + + public string GetName(MouthShape mouthShape) { + string name = MouthShapeCasing == MouthShapeCasing.Upper + ? mouthShape.ToString() + : mouthShape.ToString().ToLowerInvariant(); + return $"{Prefix}{name}{Suffix}"; + } + + private static MouthShapeCasing GuessMouthShapeCasing(char mouthShape) { + return char.IsUpper(mouthShape) ? MouthShapeCasing.Upper : MouthShapeCasing.Lower; + } + } + + public enum MouthShapeCasing { + Upper, + Lower + } +} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/MouthShape.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/MouthShape.cs new file mode 100644 index 0000000..0e15ee3 --- /dev/null +++ b/extras/rhubarb-for-spine/rhubarb-for-spine/MouthShape.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace rhubarb_for_spine { + public enum MouthShape { + A, B, C, D, E, F, G, H, X + } + + public static class MouthShapes { + public static IReadOnlyCollection All => + Enum.GetValues(typeof(MouthShape)) + .Cast() + .ToList(); + + public const int BasicShapesCount = 6; + + public static bool IsBasic(MouthShape mouthShape) => + (int) mouthShape < BasicShapesCount; + + public static IReadOnlyCollection Basic => + All.Take(BasicShapesCount).ToList(); + } +} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/ProcessTools.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/ProcessTools.cs new file mode 100644 index 0000000..7e3a135 --- /dev/null +++ b/extras/rhubarb-for-spine/rhubarb-for-spine/ProcessTools.cs @@ -0,0 +1,70 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Nito.AsyncEx; + +namespace rhubarb_for_spine { + public static class ProcessTools { + + public static async Task RunProcessAsync( + string processFilePath, + string processArgs, + Action receiveStdout, + Action receiveStderr, + CancellationToken cancellationToken + ) { + var startInfo = new ProcessStartInfo { + FileName = processFilePath, + Arguments = processArgs, + UseShellExecute = false, // Necessary to redirect streams + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + using (var process = new Process { StartInfo = startInfo, EnableRaisingEvents = true }) { + // Process all events in the original call's context + var pendingActions = new AsyncProducerConsumerQueue(); + int? exitCode = null; + + // ReSharper disable once AccessToDisposedClosure + process.Exited += (sender, args) => + pendingActions.Enqueue(() => exitCode = process.ExitCode); + + process.OutputDataReceived += (sender, args) => { + if (args.Data != null) { + pendingActions.Enqueue(() => receiveStdout(args.Data)); + } + }; + process.ErrorDataReceived += (sender, args) => { + if (args.Data != null) { + pendingActions.Enqueue(() => receiveStderr(args.Data)); + } + }; + + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + cancellationToken.Register(() => pendingActions.Enqueue(() => { + try { + // ReSharper disable once AccessToDisposedClosure + process.Kill(); + } catch (Exception e) { + Debug.WriteLine($"Error terminating process: {e}"); + } + // ReSharper disable once AccessToDisposedClosure + process.WaitForExit(); + throw new OperationCanceledException(); + })); + + while (exitCode == null) { + Action action = await pendingActions.DequeueAsync(cancellationToken); + action(); + } + return exitCode.Value; + } + } + + } +} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/Program.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/Program.cs new file mode 100644 index 0000000..dc1e8e3 --- /dev/null +++ b/extras/rhubarb-for-spine/rhubarb-for-spine/Program.cs @@ -0,0 +1,16 @@ +using System; +using System.Linq; +using System.Windows.Forms; + +namespace rhubarb_for_spine { + static class Program { + [STAThread] + static void Main(string[] args) { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + string filePath = args.FirstOrDefault(); + MainModel mainModel = new MainModel(filePath); + Application.Run(new MainForm(mainModel)); + } + } +} diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/Properties/AssemblyInfo.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..795df04 --- /dev/null +++ b/extras/rhubarb-for-spine/rhubarb-for-spine/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("rhubarb-for-spine")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("rhubarb-for-spine")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("c5ed6f8a-6141-4bae-be24-77dee23e495f")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/Properties/DataSources/MainModel.datasource b/extras/rhubarb-for-spine/rhubarb-for-spine/Properties/DataSources/MainModel.datasource new file mode 100644 index 0000000..d8a8c22 --- /dev/null +++ b/extras/rhubarb-for-spine/rhubarb-for-spine/Properties/DataSources/MainModel.datasource @@ -0,0 +1,10 @@ + + + + rhubarb_for_spine.MainModel, rhubarb-for-spine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/Properties/Settings.Designer.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/Properties/Settings.Designer.cs new file mode 100644 index 0000000..b80fcd7 --- /dev/null +++ b/extras/rhubarb-for-spine/rhubarb-for-spine/Properties/Settings.Designer.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace rhubarb_for_spine.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + } +} diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/Properties/Settings.settings b/extras/rhubarb-for-spine/rhubarb-for-spine/Properties/Settings.settings new file mode 100644 index 0000000..3964565 --- /dev/null +++ b/extras/rhubarb-for-spine/rhubarb-for-spine/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/RhubarbCli.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/RhubarbCli.cs new file mode 100644 index 0000000..6e1e4c6 --- /dev/null +++ b/extras/rhubarb-for-spine/rhubarb-for-spine/RhubarbCli.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; + +namespace rhubarb_for_spine { + public static class RhubarbCli { + public static async Task> AnimateAsync( + string audioFilePath, string dialog, ISet extendedMouthShapes, + IProgress progress, CancellationToken cancellationToken + ) { + if (cancellationToken.IsCancellationRequested) { + throw new OperationCanceledException(); + } + if (!File.Exists(audioFilePath)) { + throw new ArgumentException($"File '{audioFilePath}' does not exist."); + } + + using (var dialogFile = dialog != null ? new TemporaryTextFile(dialog) : null) { + string rhubarbExePath = GetRhubarbExePath(); + string args = CreateArgs(audioFilePath, extendedMouthShapes, dialogFile?.FilePath); + + bool success = false; + string errorMessage = null; + string resultString = ""; + + await ProcessTools.RunProcessAsync( + rhubarbExePath, args, + outString => resultString += outString, + errString => { + dynamic json = JObject.Parse(errString); + switch ((string) json.type) { + case "progress": + progress.Report((double) json.value); + break; + case "success": + success = true; + break; + case "failure": + errorMessage = json.reason; + break; + } + }, + cancellationToken); + + if (errorMessage != null) { + throw new ApplicationException(errorMessage); + } + if (success) { + progress.Report(1.0); + return ParseRhubarbResult(resultString); + } + throw new ApplicationException("Rhubarb did not return a result."); + } + } + + private static string CreateArgs( + string audioFilePath, ISet extendedMouthShapes, string dialogFilePath + ) { + string extendedShapesString = + string.Join("", extendedMouthShapes.Select(shape => shape.ToString())); + string args = "--machineReadable" + + " --exportFormat json" + + $" --extendedShapes \"{extendedShapesString}\"" + + $" \"{audioFilePath}\""; + if (dialogFilePath != null) { + args = $"--dialogFile \"{dialogFilePath}\" " + args; + } + return args; + } + + private static IReadOnlyCollection ParseRhubarbResult(string jsonString) { + dynamic json = JObject.Parse(jsonString); + JArray mouthCues = json.mouthCues; + return mouthCues + .Cast() + .Select(mouthCue => { + TimeSpan time = TimeSpan.FromSeconds((double) mouthCue.start); + MouthShape mouthShape = (MouthShape) Enum.Parse(typeof(MouthShape), (string) mouthCue.value); + return new MouthCue(time, mouthShape); + }) + .ToList(); + } + + private static string GetRhubarbExePath() { + bool onUnix = Environment.OSVersion.Platform == PlatformID.Unix + || Environment.OSVersion.Platform == PlatformID.MacOSX; + string exeName = "rhubarb" + (onUnix ? "" : ".exe"); + string guiExeDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + string currentDirectory = guiExeDirectory; + while (currentDirectory != Path.GetPathRoot(guiExeDirectory)) { + string candidate = Path.Combine(currentDirectory, exeName); + if (File.Exists(candidate)) { + return candidate; + } + currentDirectory = Path.GetDirectoryName(currentDirectory); + } + throw new ApplicationException( + $"Could not find Rhubarb Lip Sync executable '{exeName}'." + + $" Expected to find it in '{guiExeDirectory}' or any directory above."); + } + + private class TemporaryTextFile : IDisposable { + public string FilePath { get; } + + public TemporaryTextFile(string text) { + FilePath = Path.GetTempFileName(); + File.WriteAllText(FilePath, text); + } + + public void Dispose() { + File.Delete(FilePath); + } + } + } +} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/SpineJson.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/SpineJson.cs new file mode 100644 index 0000000..dcf1024 --- /dev/null +++ b/extras/rhubarb-for-spine/rhubarb-for-spine/SpineJson.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Newtonsoft.Json.Linq; + +namespace rhubarb_for_spine { + public static class SpineJson { + public static JObject ReadJson(string filePath) { + string jsonString = File.ReadAllText(filePath); + try { + return JObject.Parse(jsonString); + } catch (Exception) { + throw new ApplicationException("Wrong file format. This is not a valid JSON file."); + } + } + + public static void ValidateJson(JObject json, string animationFileDirectory) { + // This method doesn't validate the entire JSON. + // It merely checks that there are no obvious problems. + + dynamic skeleton = ((dynamic) json).skeleton; + if (skeleton == null) { + throw new ApplicationException("JSON file is corrupted."); + } + if (skeleton.images == null) { + throw new ApplicationException( + "JSON file is incomplete. Make sure you checked 'Nonessential data' when exporting."); + } + if (((dynamic) json).skins["default"] == null) { + throw new ApplicationException("JSON file has no default skin."); + } + GetImagesDirectory(json, animationFileDirectory); + GetAudioDirectory(json, animationFileDirectory); + } + + public static string GetImagesDirectory(JObject json, string animationFileDirectory) { + string result = Path.GetFullPath(Path.Combine(animationFileDirectory, (string) ((dynamic) json).skeleton.images)); + if (!Directory.Exists(result)) { + throw new ApplicationException( + "Could not find images directory relative to the JSON file." + + " Make sure the JSON file is in the same directory as the original Spine file."); + } + return result; + } + + public static string GetAudioDirectory(JObject json, string animationFileDirectory) { + string result = Path.GetFullPath(Path.Combine(animationFileDirectory, (string) ((dynamic) json).skeleton.audio)); + if (!Directory.Exists(result)) { + throw new ApplicationException( + "Could not find audio directory relative to the JSON file." + + " Make sure the JSON file is in the same directory as the original Spine file."); + } + return result; + } + + public static double GetFrameRate(JObject json) { + return (double?) ((dynamic) json).skeleton.fps ?? 30.0; + } + + public static IReadOnlyCollection GetSlots(JObject json) { + return ((JArray) ((dynamic) json).slots) + .Cast() + .Select(slot => (string) slot.name) + .ToList(); + } + + public static string GuessMouthSlot(IReadOnlyCollection slots) { + return slots.FirstOrDefault(slot => slot.Contains("mouth", StringComparison.InvariantCultureIgnoreCase)) + ?? slots.FirstOrDefault(); + } + + public class AudioEvent { + public AudioEvent(string name, string relativeAudioFilePath, string dialog) { + Name = name; + RelativeAudioFilePath = relativeAudioFilePath; + Dialog = dialog; + } + + public string Name { get; } + public string RelativeAudioFilePath { get; } + public string Dialog { get; } + } + + public static IReadOnlyCollection GetAudioEvents(JObject json) { + return ((IEnumerable>) ((dynamic) json).events) + .Select(pair => { + string name = pair.Key; + dynamic value = pair.Value; + string relativeAudioFilePath = value.audio; + string dialog = value["string"]; + return new AudioEvent(name, relativeAudioFilePath, dialog); + }) + .Where(audioEvent => audioEvent.RelativeAudioFilePath != null) + .ToList(); + } + + public static IReadOnlyCollection GetSlotAttachmentNames(JObject json, string slotName) { + return ((JObject) ((dynamic) json).skins["default"][slotName]) + .Properties() + .Select(property => property.Name) + .ToList(); + } + + public static bool HasAnimation(JObject json, string animationName) { + JObject animations = ((dynamic) json).animations; + if (animations == null) return false; + + return animations.Properties().Any(property => property.Name == animationName); + } + + public static void CreateOrUpdateAnimation( + JObject json, IReadOnlyCollection animationResult, + string eventName, string animationName, string mouthSlot, MouthNaming mouthNaming + ) { + dynamic dynamicJson = json; + dynamic animations = dynamicJson.animations; + if (animations == null) { + animations = dynamicJson.animations = new JObject(); + } + + // Round times to full frame. Always round down. + // If events coincide, prefer the later one. + double frameRate = GetFrameRate(json); + var keyframes = new Dictionary(); + foreach (MouthCue mouthCue in animationResult) { + int frameNumber = (int) (mouthCue.Time.TotalSeconds * frameRate); + keyframes[frameNumber] = mouthCue.MouthShape; + } + + animations[animationName] = new JObject { + ["slots"] = new JObject { + [mouthSlot] = new JObject { + ["attachment"] = new JArray( + keyframes + .OrderBy(pair => pair.Key) + .Select(pair => new JObject { + ["time"] = pair.Key / frameRate, + ["name"] = mouthNaming.GetName(pair.Value) + }) + .Cast() + .ToArray() + ) + } + }, + ["events"] = new JArray { + new JObject { + ["time"] = 0.0, + ["name"] = eventName, + ["string"] = "" + } + } + }; + } + } +} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/Tools.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/Tools.cs new file mode 100644 index 0000000..3540d7e --- /dev/null +++ b/extras/rhubarb-for-spine/rhubarb-for-spine/Tools.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; + +namespace rhubarb_for_spine { + public static class Tools { + public static string GetCommonPrefix(this IReadOnlyCollection strings) { + return strings.Any() + ? strings.First().Substring(0, GetCommonPrefixLength(strings)) + : string.Empty; + } + + public static int GetCommonPrefixLength(this IReadOnlyCollection strings) { + if (!strings.Any()) return 0; + + string first = strings.First(); + int result = first.Length; + foreach (string s in strings) { + for (int i = 0; i < Math.Min(result, s.Length); i++) { + if (s[i] != first[i]) { + result = i; + break; + } + } + } + return result; + } + + public static string GetCommonSuffix(this IReadOnlyCollection strings) { + if (!strings.Any()) return string.Empty; + + int commonSuffixLength = GetCommonSuffixLength(strings); + string first = strings.First(); + return first.Substring(first.Length - commonSuffixLength); + } + + public static int GetCommonSuffixLength(this IReadOnlyCollection strings) { + if (!strings.Any()) return 0; + + string first = strings.First(); + int result = first.Length; + foreach (string s in strings) { + for (int i = 0; i < Math.Min(result, s.Length); i++) { + if (s[s.Length - 1 - i] != first[first.Length - 1 - i]) { + result = i; + break; + } + } + } + return result; + } + + public static bool Contains(this string s, string value, StringComparison stringComparison) { + return s.IndexOf(value, stringComparison) >= 0; + } + + public static string Join(this IEnumerable values, string separator) { + return string.Join(separator, values); + } + + public static Color WithOpacity(this Color color, double opacity) { + return Color.FromArgb((int) (opacity * 255), color); + } + } + } \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/app.config b/extras/rhubarb-for-spine/rhubarb-for-spine/app.config new file mode 100644 index 0000000..b45f31e --- /dev/null +++ b/extras/rhubarb-for-spine/rhubarb-for-spine/app.config @@ -0,0 +1,3 @@ + + + diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/packages.config b/extras/rhubarb-for-spine/rhubarb-for-spine/packages.config new file mode 100644 index 0000000..f2cad59 --- /dev/null +++ b/extras/rhubarb-for-spine/rhubarb-for-spine/packages.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/rhubarb-for-spine.csproj b/extras/rhubarb-for-spine/rhubarb-for-spine/rhubarb-for-spine.csproj new file mode 100644 index 0000000..ad62196 --- /dev/null +++ b/extras/rhubarb-for-spine/rhubarb-for-spine/rhubarb-for-spine.csproj @@ -0,0 +1,138 @@ + + + + + Debug + AnyCPU + {C5ED6F8A-6141-4BAE-BE24-77DEE23E495F} + WinExe + Properties + rhubarb_for_spine + rhubarb-for-spine + v4.6 + 512 + + + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + rhubarb.ico + + + + + ..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.dll + True + + + ..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.dll + True + + + ..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.Desktop.dll + True + + + ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll + True + + + ..\packages\Nito.AsyncEx.4.0.1\lib\net45\Nito.AsyncEx.dll + True + + + ..\packages\Nito.AsyncEx.4.0.1\lib\net45\Nito.AsyncEx.Concurrent.dll + True + + + ..\packages\Nito.AsyncEx.4.0.1\lib\net45\Nito.AsyncEx.Enlightenment.dll + True + + + + + + + + + + + + + + + Component + + + Form + + + MainForm.cs + + + + + + + + + + + + + + MainForm.cs + + + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + Settings.settings + True + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/rhubarb.ico b/extras/rhubarb-for-spine/rhubarb-for-spine/rhubarb.ico new file mode 100644 index 0000000000000000000000000000000000000000..0e9923f42e7e91f50221ea9229b59696514d9fb3 GIT binary patch literal 21947 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 literal 0 HcmV?d00001 From a3252359fb84a88c3226d432ff3702dcf5d24c43 Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Tue, 14 Nov 2017 20:29:25 +0100 Subject: [PATCH 04/33] Removed rhubarb-for-spine. Wrong tech stack. Turns out I chose the wrong technology stack. Windows Forms is riddled with bugs that Microsoft won't fix, and Mono's support for Windows Forms isn't quite the drop-in replacement I had hoped for. --- extras/rhubarb-for-spine/.gitignore | 3 - .../rhubarb-for-spine/rhubarb-for-spine.sln | 22 - .../rhubarb-for-spine/AnimationFileModel.cs | 147 ----- .../rhubarb-for-spine/AudioFileModel.cs | 136 ----- .../rhubarb-for-spine/BindableComboBox.cs | 30 - .../rhubarb-for-spine/MainForm.Designer.cs | 366 ------------ .../rhubarb-for-spine/MainForm.cs | 107 ---- .../rhubarb-for-spine/MainForm.resx | 521 ------------------ .../rhubarb-for-spine/MainModel.cs | 50 -- .../rhubarb-for-spine/ModelBase.cs | 24 - .../rhubarb-for-spine/MouthCue.cs | 17 - .../rhubarb-for-spine/MouthNaming.cs | 60 -- .../rhubarb-for-spine/MouthShape.cs | 24 - .../rhubarb-for-spine/ProcessTools.cs | 70 --- .../rhubarb-for-spine/Program.cs | 16 - .../Properties/AssemblyInfo.cs | 36 -- .../DataSources/MainModel.datasource | 10 - .../Properties/Settings.Designer.cs | 26 - .../Properties/Settings.settings | 7 - .../rhubarb-for-spine/RhubarbCli.cs | 120 ---- .../rhubarb-for-spine/SpineJson.cs | 156 ------ .../rhubarb-for-spine/Tools.cs | 66 --- .../rhubarb-for-spine/app.config | 3 - .../rhubarb-for-spine/packages.config | 8 - .../rhubarb-for-spine.csproj | 138 ----- .../rhubarb-for-spine/rhubarb.ico | Bin 21947 -> 0 bytes 26 files changed, 2163 deletions(-) delete mode 100644 extras/rhubarb-for-spine/.gitignore delete mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine.sln delete mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/AnimationFileModel.cs delete mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/AudioFileModel.cs delete mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/BindableComboBox.cs delete mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/MainForm.Designer.cs delete mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/MainForm.cs delete mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/MainForm.resx delete mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/MainModel.cs delete mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/ModelBase.cs delete mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/MouthCue.cs delete mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/MouthNaming.cs delete mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/MouthShape.cs delete mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/ProcessTools.cs delete mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/Program.cs delete mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/Properties/AssemblyInfo.cs delete mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/Properties/DataSources/MainModel.datasource delete mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/Properties/Settings.Designer.cs delete mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/Properties/Settings.settings delete mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/RhubarbCli.cs delete mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/SpineJson.cs delete mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/Tools.cs delete mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/app.config delete mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/packages.config delete mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/rhubarb-for-spine.csproj delete mode 100644 extras/rhubarb-for-spine/rhubarb-for-spine/rhubarb.ico diff --git a/extras/rhubarb-for-spine/.gitignore b/extras/rhubarb-for-spine/.gitignore deleted file mode 100644 index cf7bd3f..0000000 --- a/extras/rhubarb-for-spine/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -bin/ -obj/ -packages/ \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine.sln b/extras/rhubarb-for-spine/rhubarb-for-spine.sln deleted file mode 100644 index 6d9b2c3..0000000 --- a/extras/rhubarb-for-spine/rhubarb-for-spine.sln +++ /dev/null @@ -1,22 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25420.1 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "rhubarb-for-spine", "rhubarb-for-spine\rhubarb-for-spine.csproj", "{C5ED6F8A-6141-4BAE-BE24-77DEE23E495F}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {C5ED6F8A-6141-4BAE-BE24-77DEE23E495F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C5ED6F8A-6141-4BAE-BE24-77DEE23E495F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C5ED6F8A-6141-4BAE-BE24-77DEE23E495F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C5ED6F8A-6141-4BAE-BE24-77DEE23E495F}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/AnimationFileModel.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/AnimationFileModel.cs deleted file mode 100644 index 929f48c..0000000 --- a/extras/rhubarb-for-spine/rhubarb-for-spine/AnimationFileModel.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace rhubarb_for_spine { - public class AnimationFileModel : ModelBase { - private readonly string animationFilePath; - private readonly SemaphoreSlim semaphore; - private readonly JObject json; - - private string _mouthSlot; - private MouthNaming _mouthNaming; - private IReadOnlyCollection _mouthShapes; - private string _mouthShapesDisplayString; - private BindingList _audioFileModels; - - public AnimationFileModel(string animationFilePath, SemaphoreSlim semaphore) { - this.animationFilePath = animationFilePath; - this.semaphore = semaphore; - json = SpineJson.ReadJson(animationFilePath); - SpineJson.ValidateJson(json, AnimationFileDirectory); - - Slots = SpineJson.GetSlots(json); - MouthSlot = SpineJson.GuessMouthSlot(Slots); - var audioFileModels = SpineJson.GetAudioEvents(json) - .Select(CreateAudioFileModel) - .ToList(); - AudioFileModels = new BindingList(audioFileModels); - } - - public IReadOnlyCollection Slots { get; } - - public string MouthSlot { - get { return _mouthSlot; } - set { - _mouthSlot = value; - MouthNaming = GetMouthNaming(); - MouthShapes = GetMouthShapes(); - OnPropertyChanged(nameof(MouthSlot)); - } - } - - public MouthNaming MouthNaming { - get { return _mouthNaming; } - private set { - _mouthNaming = value; - OnPropertyChanged(nameof(MouthNaming)); - } - } - - public IReadOnlyCollection MouthShapes { - get { return _mouthShapes; } - private set { - _mouthShapes = value; - MouthShapesDisplayString = GetMouthShapesDisplayString(); - OnPropertyChanged(nameof(MouthShapes)); - } - } - - public string MouthShapesDisplayString { - get { return _mouthShapesDisplayString; } - set { - _mouthShapesDisplayString = value; - SetError(nameof(MouthShapesDisplayString), GetMouthShapesDisplayStringError()); - OnPropertyChanged(nameof(MouthShapesDisplayString)); - } - } - - public BindingList AudioFileModels { - get { return _audioFileModels; } - set { - _audioFileModels = value; - if (!_audioFileModels.Any()) { - SetError(nameof(AudioFileModels), "The JSON file doesn't contain any audio events."); - } - OnPropertyChanged(nameof(AudioFileModels)); - } - } - - private string AnimationFileDirectory => Path.GetDirectoryName(animationFilePath); - - private MouthNaming GetMouthNaming() { - IReadOnlyCollection mouthNames = SpineJson.GetSlotAttachmentNames(json, _mouthSlot); - return MouthNaming.Guess(mouthNames); - } - - private List GetMouthShapes() { - IReadOnlyCollection mouthNames = SpineJson.GetSlotAttachmentNames(json, _mouthSlot); - return rhubarb_for_spine.MouthShapes.All - .Where(shape => mouthNames.Contains(MouthNaming.GetName(shape))) - .ToList(); - } - - private string GetMouthShapesDisplayString() { - return MouthShapes - .Select(shape => shape.ToString()) - .Join(", "); - } - - private string GetMouthShapesDisplayStringError() { - var basicMouthShapes = rhubarb_for_spine.MouthShapes.Basic; - var missingBasicShapes = basicMouthShapes - .Where(shape => !MouthShapes.Contains(shape)) - .ToList(); - if (!missingBasicShapes.Any()) return null; - - var result = new StringBuilder(); - string missingString = string.Join(", ", missingBasicShapes); - result.AppendLine(missingBasicShapes.Count > 1 - ? $"Mouth shapes {missingString} are missing." - : $"Mouth shape {missingString} is missing."); - MouthShape first = basicMouthShapes.First(); - MouthShape last = basicMouthShapes.Last(); - result.Append($"At least the basic mouth shapes {first}-{last} need corresponding image attachments."); - return result.ToString(); - } - - private AudioFileModel CreateAudioFileModel(SpineJson.AudioEvent audioEvent) { - string audioDirectory = SpineJson.GetAudioDirectory(json, AnimationFileDirectory); - string filePath = Path.Combine(audioDirectory, audioEvent.RelativeAudioFilePath); - bool animatedPreviously = SpineJson.HasAnimation(json, GetAnimationName(audioEvent.Name)); - var extendedMouthShapes = MouthShapes.Where(shape => !rhubarb_for_spine.MouthShapes.IsBasic(shape)); - return new AudioFileModel( - audioEvent.Name, filePath, audioEvent.RelativeAudioFilePath, audioEvent.Dialog, - new HashSet(extendedMouthShapes), animatedPreviously, - semaphore, cues => AcceptAnimationResult(cues, audioEvent)); - } - - private string GetAnimationName(string audioEventName) { - return $"say_{audioEventName}"; - } - - private void AcceptAnimationResult( - IReadOnlyCollection mouthCues, SpineJson.AudioEvent audioEvent - ) { - string animationName = GetAnimationName(audioEvent.Name); - SpineJson.CreateOrUpdateAnimation( - json, mouthCues, audioEvent.Name, animationName, MouthSlot, MouthNaming); - File.WriteAllText(animationFilePath, json.ToString(Formatting.Indented)); - } - } -} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/AudioFileModel.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/AudioFileModel.cs deleted file mode 100644 index dff3f26..0000000 --- a/extras/rhubarb-for-spine/rhubarb-for-spine/AudioFileModel.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; - -namespace rhubarb_for_spine { - public class AudioFileModel : ModelBase { - private readonly string filePath; - private readonly ISet extendedMouthShapes; - private readonly SemaphoreSlim semaphore; - private readonly Action> reportResult; - private bool animatedPreviously; - private CancellationTokenSource cancellationTokenSource; - - private AudioFileStatus _status; - private string _actionLabel; - private double? _animationProgress; - - public AudioFileModel( - string name, - string filePath, - string displayFilePath, - string dialog, - ISet extendedMouthShapes, - bool animatedPreviously, - SemaphoreSlim semaphore, - Action> reportResult - ) { - this.reportResult = reportResult; - Name = name; - this.filePath = filePath; - this.extendedMouthShapes = extendedMouthShapes; - this.animatedPreviously = animatedPreviously; - this.semaphore = semaphore; - DisplayFilePath = displayFilePath; - Dialog = dialog; - Status = animatedPreviously ? AudioFileStatus.Done : AudioFileStatus.NotAnimated; - } - - public string Name { get; } - public string DisplayFilePath { get; } - public string Dialog { get; } - - public double? AnimationProgress { - get { return _animationProgress; } - private set { - _animationProgress = value; - OnPropertyChanged(nameof(AnimationProgress)); - - // Hack, so that a binding to Status will refresh - OnPropertyChanged(nameof(Status)); - } - } - - public AudioFileStatus Status { - get { return _status; } - private set { - _status = value; - ActionLabel = _status == AudioFileStatus.Scheduled || _status == AudioFileStatus.Animating - ? "Cancel" - : "Animate"; - OnPropertyChanged(nameof(Status)); - } - } - - public string ActionLabel { - get { return _actionLabel; } - private set { - _actionLabel = value; - OnPropertyChanged(nameof(ActionLabel)); - } - } - - public void PerformAction() { - switch (Status) { - case AudioFileStatus.NotAnimated: - case AudioFileStatus.Done: - StartAnimation(); - break; - case AudioFileStatus.Scheduled: - case AudioFileStatus.Animating: - CancelAnimation(); - break; - default: - throw new ArgumentOutOfRangeException(); - } - } - - private async void StartAnimation() { - cancellationTokenSource = new CancellationTokenSource(); - Status = AudioFileStatus.Scheduled; - try { - await semaphore.WaitAsync(cancellationTokenSource.Token); - Status = AudioFileStatus.Animating; - try { - var progress = new Progress(value => AnimationProgress = value); - IReadOnlyCollection mouthCues = await RhubarbCli.AnimateAsync( - filePath, Dialog, extendedMouthShapes, progress, cancellationTokenSource.Token); - animatedPreviously = true; - Status = AudioFileStatus.Done; - reportResult(mouthCues); - } finally { - AnimationProgress = null; - semaphore.Release(); - cancellationTokenSource = null; - } - } catch (OperationCanceledException) { - Status = animatedPreviously ? AudioFileStatus.Done : AudioFileStatus.NotAnimated; - } - } - - private void CancelAnimation() { - cancellationTokenSource.Cancel(); - } - - private class Progress : IProgress { - private readonly Action report; - private double value; - - public Progress(Action report) { - this.report = report; - } - - public void Report(double progress) { - value = progress; - report(progress); - } - } - } - - public enum AudioFileStatus { - NotAnimated, - Scheduled, - Animating, - Done - } -} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/BindableComboBox.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/BindableComboBox.cs deleted file mode 100644 index c619ed5..0000000 --- a/extras/rhubarb-for-spine/rhubarb-for-spine/BindableComboBox.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Linq; -using System.Windows.Forms; - -namespace rhubarb_for_spine { - /// - /// A modification of the standard in which a data binding - /// on the SelectedItem property with the update mode set to DataSourceUpdateMode.OnPropertyChanged - /// actually updates when a selection is made in the combobox. - /// Code taken from https://stackoverflow.com/a/8392100/52041 - /// - public class BindableComboBox : ComboBox { - /// - protected override void OnSelectionChangeCommitted(EventArgs e) { - base.OnSelectionChangeCommitted(e); - - var bindings = DataBindings - .Cast() - .Where(binding => binding.PropertyName == nameof(SelectedItem) - && binding.DataSourceUpdateMode == DataSourceUpdateMode.OnPropertyChanged); - foreach (Binding binding in bindings) { - // Force the binding to update from the new SelectedItem - binding.WriteValue(); - - // Force the Textbox to update from the binding - binding.ReadValue(); - } - } - } -} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/MainForm.Designer.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/MainForm.Designer.cs deleted file mode 100644 index 20e4089..0000000 --- a/extras/rhubarb-for-spine/rhubarb-for-spine/MainForm.Designer.cs +++ /dev/null @@ -1,366 +0,0 @@ -using System; - -namespace rhubarb_for_spine { - partial class MainForm { - - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) { - if (disposing && (components != null)) { - components.Dispose(); - } - base.Dispose(disposing); - } - - protected override void OnLoad(EventArgs e) { - base.OnLoad(e); - - // Some bindings cannot be added in InitializeComponent; see https://stackoverflow.com/a/47167781/52041. - mouthSlotComboBox.DataBindings.Add(new System.Windows.Forms.Binding("DataSource", animationFileBindingSource, "Slots", true)); - mouthSlotComboBox.DataBindings.Add(new System.Windows.Forms.Binding("SelectedItem", animationFileBindingSource, "MouthSlot", true, System.Windows.Forms.DataSourceUpdateMode.OnPropertyChanged)); - audioEventsDataGridView.AutoGenerateColumns = false; - audioEventsDataGridView.DataBindings.Add(new System.Windows.Forms.Binding("DataSource", animationFileBindingSource, "AudioFileModels", true)); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() { - this.components = new System.ComponentModel.Container(); - System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle2 = new System.Windows.Forms.DataGridViewCellStyle(); - System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle(); - System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm)); - this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); - this.filePathLabel = new System.Windows.Forms.Label(); - this.panel1 = new System.Windows.Forms.Panel(); - this.filePathTextBox = new System.Windows.Forms.TextBox(); - this.filePathBrowseButton = new System.Windows.Forms.Button(); - this.mouthSlotLabel = new System.Windows.Forms.Label(); - this.animationFileBindingSource = new System.Windows.Forms.BindingSource(this.components); - this.mouthShapesLabel = new System.Windows.Forms.Label(); - this.audioEventsLabel = new System.Windows.Forms.Label(); - this.audioEventsDataGridView = new System.Windows.Forms.DataGridView(); - this.mouthShapesResultLabel = new System.Windows.Forms.Label(); - this.mouthNamingLabel = new System.Windows.Forms.Label(); - this.mouthNamingResultLabel = new System.Windows.Forms.Label(); - this.mainErrorProvider = new System.Windows.Forms.ErrorProvider(this.components); - this.animationFileErrorProvider = new System.Windows.Forms.ErrorProvider(this.components); - this.mainBindingSource = new System.Windows.Forms.BindingSource(this.components); - this.mouthSlotComboBox = new rhubarb_for_spine.BindableComboBox(); - this.eventColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.audioFileColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.dialogColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.statusColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.actionColumn = new System.Windows.Forms.DataGridViewButtonColumn(); - this.tableLayoutPanel1.SuspendLayout(); - this.panel1.SuspendLayout(); - ((System.ComponentModel.ISupportInitialize)(this.animationFileBindingSource)).BeginInit(); - ((System.ComponentModel.ISupportInitialize)(this.audioEventsDataGridView)).BeginInit(); - ((System.ComponentModel.ISupportInitialize)(this.mainErrorProvider)).BeginInit(); - ((System.ComponentModel.ISupportInitialize)(this.animationFileErrorProvider)).BeginInit(); - ((System.ComponentModel.ISupportInitialize)(this.mainBindingSource)).BeginInit(); - this.SuspendLayout(); - // - // tableLayoutPanel1 - // - this.tableLayoutPanel1.AutoSize = true; - this.tableLayoutPanel1.ColumnCount = 2; - this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); - this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); - this.tableLayoutPanel1.Controls.Add(this.filePathLabel, 0, 0); - this.tableLayoutPanel1.Controls.Add(this.panel1, 1, 0); - this.tableLayoutPanel1.Controls.Add(this.mouthSlotLabel, 0, 1); - this.tableLayoutPanel1.Controls.Add(this.mouthSlotComboBox, 1, 1); - this.tableLayoutPanel1.Controls.Add(this.mouthShapesLabel, 0, 3); - this.tableLayoutPanel1.Controls.Add(this.audioEventsLabel, 0, 4); - this.tableLayoutPanel1.Controls.Add(this.audioEventsDataGridView, 1, 4); - this.tableLayoutPanel1.Controls.Add(this.mouthShapesResultLabel, 1, 3); - this.tableLayoutPanel1.Controls.Add(this.mouthNamingLabel, 0, 2); - this.tableLayoutPanel1.Controls.Add(this.mouthNamingResultLabel, 1, 2); - this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill; - this.tableLayoutPanel1.Location = new System.Drawing.Point(5, 5); - this.tableLayoutPanel1.Name = "tableLayoutPanel1"; - this.tableLayoutPanel1.Padding = new System.Windows.Forms.Padding(3); - this.tableLayoutPanel1.RowCount = 5; - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 20F)); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 20F)); - this.tableLayoutPanel1.Size = new System.Drawing.Size(903, 612); - this.tableLayoutPanel1.TabIndex = 0; - // - // filePathLabel - // - this.filePathLabel.AutoSize = true; - this.filePathLabel.Location = new System.Drawing.Point(6, 3); - this.filePathLabel.Name = "filePathLabel"; - this.filePathLabel.Padding = new System.Windows.Forms.Padding(0, 6, 0, 0); - this.filePathLabel.Size = new System.Drawing.Size(81, 19); - this.filePathLabel.TabIndex = 0; - this.filePathLabel.Text = "Spine JSON file"; - // - // panel1 - // - this.panel1.AutoSize = true; - this.panel1.Controls.Add(this.filePathTextBox); - this.panel1.Controls.Add(this.filePathBrowseButton); - this.panel1.Dock = System.Windows.Forms.DockStyle.Top; - this.panel1.Location = new System.Drawing.Point(93, 6); - this.panel1.Name = "panel1"; - this.panel1.Size = new System.Drawing.Size(804, 23); - this.panel1.TabIndex = 1; - // - // filePathTextBox - // - this.filePathTextBox.DataBindings.Add(new System.Windows.Forms.Binding("Text", this.mainBindingSource, "FilePath", true, System.Windows.Forms.DataSourceUpdateMode.OnPropertyChanged)); - this.mainErrorProvider.SetIconPadding(this.filePathTextBox, -20); - this.filePathTextBox.Location = new System.Drawing.Point(0, 0); - this.filePathTextBox.Name = "filePathTextBox"; - this.filePathTextBox.Size = new System.Drawing.Size(769, 20); - this.filePathTextBox.TabIndex = 2; - // - // filePathBrowseButton - // - this.filePathBrowseButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); - this.filePathBrowseButton.Location = new System.Drawing.Point(775, 0); - this.filePathBrowseButton.Name = "filePathBrowseButton"; - this.filePathBrowseButton.Size = new System.Drawing.Size(30, 20); - this.filePathBrowseButton.TabIndex = 3; - this.filePathBrowseButton.Text = "..."; - this.filePathBrowseButton.UseVisualStyleBackColor = true; - this.filePathBrowseButton.Click += new System.EventHandler(this.filePathBrowseButton_Click); - // - // mouthSlotLabel - // - this.mouthSlotLabel.AutoSize = true; - this.mouthSlotLabel.Location = new System.Drawing.Point(6, 32); - this.mouthSlotLabel.Name = "mouthSlotLabel"; - this.mouthSlotLabel.Padding = new System.Windows.Forms.Padding(0, 6, 0, 0); - this.mouthSlotLabel.Size = new System.Drawing.Size(56, 19); - this.mouthSlotLabel.TabIndex = 2; - this.mouthSlotLabel.Text = "Mouth slot"; - // - // animationFileBindingSource - // - this.animationFileBindingSource.DataMember = "AnimationFileModel"; - this.animationFileBindingSource.DataSource = this.mainBindingSource; - // - // mouthShapesLabel - // - this.mouthShapesLabel.AutoSize = true; - this.mouthShapesLabel.Location = new System.Drawing.Point(6, 79); - this.mouthShapesLabel.Name = "mouthShapesLabel"; - this.mouthShapesLabel.Padding = new System.Windows.Forms.Padding(0, 6, 0, 0); - this.mouthShapesLabel.Size = new System.Drawing.Size(74, 19); - this.mouthShapesLabel.TabIndex = 4; - this.mouthShapesLabel.Text = "Mouth shapes"; - // - // audioEventsLabel - // - this.audioEventsLabel.AutoSize = true; - this.audioEventsLabel.Location = new System.Drawing.Point(6, 98); - this.audioEventsLabel.Name = "audioEventsLabel"; - this.audioEventsLabel.Padding = new System.Windows.Forms.Padding(0, 6, 0, 0); - this.audioEventsLabel.Size = new System.Drawing.Size(69, 19); - this.audioEventsLabel.TabIndex = 6; - this.audioEventsLabel.Text = "Audio events"; - // - // audioEventsDataGridView - // - this.audioEventsDataGridView.AutoSizeRowsMode = System.Windows.Forms.DataGridViewAutoSizeRowsMode.AllCellsExceptHeaders; - this.audioEventsDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; - this.audioEventsDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { - this.eventColumn, - this.audioFileColumn, - this.dialogColumn, - this.statusColumn, - this.actionColumn}); - dataGridViewCellStyle2.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft; - dataGridViewCellStyle2.BackColor = System.Drawing.SystemColors.Window; - dataGridViewCellStyle2.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - dataGridViewCellStyle2.ForeColor = System.Drawing.SystemColors.ControlText; - dataGridViewCellStyle2.SelectionBackColor = System.Drawing.SystemColors.Window; - dataGridViewCellStyle2.SelectionForeColor = System.Drawing.SystemColors.ControlText; - dataGridViewCellStyle2.WrapMode = System.Windows.Forms.DataGridViewTriState.False; - this.audioEventsDataGridView.DefaultCellStyle = dataGridViewCellStyle2; - this.audioEventsDataGridView.Dock = System.Windows.Forms.DockStyle.Fill; - this.animationFileErrorProvider.SetIconPadding(this.audioEventsDataGridView, -20); - this.audioEventsDataGridView.Location = new System.Drawing.Point(93, 101); - this.audioEventsDataGridView.Name = "audioEventsDataGridView"; - this.audioEventsDataGridView.ReadOnly = true; - this.audioEventsDataGridView.RowHeadersVisible = false; - this.audioEventsDataGridView.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect; - this.audioEventsDataGridView.Size = new System.Drawing.Size(804, 505); - this.audioEventsDataGridView.TabIndex = 7; - this.audioEventsDataGridView.CellClick += new System.Windows.Forms.DataGridViewCellEventHandler(this.audioEventsDataGridView_CellClick); - this.audioEventsDataGridView.CellPainting += new System.Windows.Forms.DataGridViewCellPaintingEventHandler(this.audioEventsDataGridView_CellPainting); - // - // mouthShapesResultLabel - // - this.mouthShapesResultLabel.AutoSize = true; - this.mouthShapesResultLabel.DataBindings.Add(new System.Windows.Forms.Binding("Text", this.animationFileBindingSource, "MouthShapesDisplayString", true)); - this.mouthShapesResultLabel.Location = new System.Drawing.Point(93, 79); - this.mouthShapesResultLabel.Name = "mouthShapesResultLabel"; - this.mouthShapesResultLabel.Padding = new System.Windows.Forms.Padding(0, 6, 0, 0); - this.mouthShapesResultLabel.Size = new System.Drawing.Size(16, 19); - this.mouthShapesResultLabel.TabIndex = 8; - this.mouthShapesResultLabel.Text = " "; - // - // mouthNamingLabel - // - this.mouthNamingLabel.AutoSize = true; - this.mouthNamingLabel.Location = new System.Drawing.Point(6, 59); - this.mouthNamingLabel.Name = "mouthNamingLabel"; - this.mouthNamingLabel.Padding = new System.Windows.Forms.Padding(0, 6, 0, 0); - this.mouthNamingLabel.Size = new System.Drawing.Size(74, 19); - this.mouthNamingLabel.TabIndex = 9; - this.mouthNamingLabel.Text = "Mouth naming"; - // - // mouthNamingResultLabel - // - this.mouthNamingResultLabel.AutoSize = true; - this.mouthNamingResultLabel.DataBindings.Add(new System.Windows.Forms.Binding("Text", this.animationFileBindingSource, "MouthNaming.DisplayString", true)); - this.mouthNamingResultLabel.Location = new System.Drawing.Point(93, 59); - this.mouthNamingResultLabel.Name = "mouthNamingResultLabel"; - this.mouthNamingResultLabel.Padding = new System.Windows.Forms.Padding(0, 6, 0, 0); - this.mouthNamingResultLabel.Size = new System.Drawing.Size(16, 19); - this.mouthNamingResultLabel.TabIndex = 10; - this.mouthNamingResultLabel.Text = " "; - // - // mainErrorProvider - // - this.mainErrorProvider.BlinkStyle = System.Windows.Forms.ErrorBlinkStyle.NeverBlink; - this.mainErrorProvider.ContainerControl = this; - this.mainErrorProvider.DataSource = this.mainBindingSource; - // - // animationFileErrorProvider - // - this.animationFileErrorProvider.BlinkStyle = System.Windows.Forms.ErrorBlinkStyle.NeverBlink; - this.animationFileErrorProvider.ContainerControl = this; - this.animationFileErrorProvider.DataSource = this.animationFileBindingSource; - // - // mainBindingSource - // - this.mainBindingSource.DataSource = typeof(rhubarb_for_spine.MainModel); - // - // mouthSlotComboBox - // - this.mouthSlotComboBox.Dock = System.Windows.Forms.DockStyle.Top; - this.mouthSlotComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; - this.mouthSlotComboBox.Location = new System.Drawing.Point(93, 35); - this.mouthSlotComboBox.Name = "mouthSlotComboBox"; - this.mouthSlotComboBox.Size = new System.Drawing.Size(804, 21); - this.mouthSlotComboBox.TabIndex = 3; - // - // eventColumn - // - this.eventColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; - this.eventColumn.DataPropertyName = "Name"; - this.eventColumn.HeaderText = "Event"; - this.eventColumn.Name = "eventColumn"; - this.eventColumn.ReadOnly = true; - // - // audioFileColumn - // - this.audioFileColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; - this.audioFileColumn.DataPropertyName = "DisplayFilePath"; - this.audioFileColumn.HeaderText = "Audio file"; - this.audioFileColumn.Name = "audioFileColumn"; - this.audioFileColumn.ReadOnly = true; - // - // dialogColumn - // - this.dialogColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; - this.dialogColumn.DataPropertyName = "Dialog"; - dataGridViewCellStyle1.WrapMode = System.Windows.Forms.DataGridViewTriState.True; - this.dialogColumn.DefaultCellStyle = dataGridViewCellStyle1; - this.dialogColumn.FillWeight = 300F; - this.dialogColumn.HeaderText = "Dialog"; - this.dialogColumn.Name = "dialogColumn"; - this.dialogColumn.ReadOnly = true; - // - // statusColumn - // - this.statusColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; - this.statusColumn.DataPropertyName = "Status"; - this.statusColumn.HeaderText = "Status"; - this.statusColumn.Name = "statusColumn"; - this.statusColumn.ReadOnly = true; - // - // actionColumn - // - this.actionColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.None; - this.actionColumn.DataPropertyName = "ActionLabel"; - this.actionColumn.HeaderText = ""; - this.actionColumn.Name = "actionColumn"; - this.actionColumn.ReadOnly = true; - this.actionColumn.Resizable = System.Windows.Forms.DataGridViewTriState.True; - this.actionColumn.Text = ""; - this.actionColumn.Width = 80; - // - // MainForm - // - this.AllowDrop = true; - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(913, 622); - this.Controls.Add(this.tableLayoutPanel1); - this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); - this.Name = "MainForm"; - this.Padding = new System.Windows.Forms.Padding(5); - this.Text = "Rhubarb Lip Sync for Spine"; - this.DragDrop += new System.Windows.Forms.DragEventHandler(this.MainForm_DragDrop); - this.DragEnter += new System.Windows.Forms.DragEventHandler(this.MainForm_DragEnter); - this.tableLayoutPanel1.ResumeLayout(false); - this.tableLayoutPanel1.PerformLayout(); - this.panel1.ResumeLayout(false); - this.panel1.PerformLayout(); - ((System.ComponentModel.ISupportInitialize)(this.animationFileBindingSource)).EndInit(); - ((System.ComponentModel.ISupportInitialize)(this.audioEventsDataGridView)).EndInit(); - ((System.ComponentModel.ISupportInitialize)(this.mainErrorProvider)).EndInit(); - ((System.ComponentModel.ISupportInitialize)(this.animationFileErrorProvider)).EndInit(); - ((System.ComponentModel.ISupportInitialize)(this.mainBindingSource)).EndInit(); - this.ResumeLayout(false); - this.PerformLayout(); - - } - - #endregion - - private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; - private System.Windows.Forms.Label filePathLabel; - private System.Windows.Forms.Panel panel1; - private System.Windows.Forms.TextBox filePathTextBox; - private System.Windows.Forms.Button filePathBrowseButton; - private System.Windows.Forms.Label mouthSlotLabel; - private BindableComboBox mouthSlotComboBox; - private System.Windows.Forms.Label mouthShapesLabel; - private System.Windows.Forms.Label audioEventsLabel; - private System.Windows.Forms.DataGridView audioEventsDataGridView; - private System.Windows.Forms.BindingSource mainBindingSource; - private System.Windows.Forms.ErrorProvider mainErrorProvider; - private System.Windows.Forms.Label mouthShapesResultLabel; - private System.Windows.Forms.Label mouthNamingLabel; - private System.Windows.Forms.Label mouthNamingResultLabel; - private System.Windows.Forms.BindingSource animationFileBindingSource; - private System.Windows.Forms.ErrorProvider animationFileErrorProvider; - private System.Windows.Forms.DataGridViewTextBoxColumn eventColumn; - private System.Windows.Forms.DataGridViewTextBoxColumn audioFileColumn; - private System.Windows.Forms.DataGridViewTextBoxColumn dialogColumn; - private System.Windows.Forms.DataGridViewTextBoxColumn statusColumn; - private System.Windows.Forms.DataGridViewButtonColumn actionColumn; - } -} - diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/MainForm.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/MainForm.cs deleted file mode 100644 index 80f3f5a..0000000 --- a/extras/rhubarb-for-spine/rhubarb-for-spine/MainForm.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using System.Drawing; -using System.Linq; -using System.Windows.Forms; - -namespace rhubarb_for_spine { - public partial class MainForm : Form { - private MainModel MainModel { get; } - - public MainForm(MainModel mainModel) { - InitializeComponent(); - MainModel = mainModel; - mainBindingSource.DataSource = mainModel; - } - - private void filePathBrowseButton_Click(object sender, EventArgs e) { - var openFileDialog = new OpenFileDialog { - Filter = "JSON files (*.json)|*.json", - InitialDirectory = filePathTextBox.Text - }; - if (openFileDialog.ShowDialog() == DialogResult.OK) { - filePathTextBox.Text = openFileDialog.FileName; - } - } - - private void MainForm_DragEnter(object sender, DragEventArgs e) { - if (e.Data.GetDataPresent(DataFormats.FileDrop)) { - e.Effect = DragDropEffects.Copy; - } - } - - private void MainForm_DragDrop(object sender, DragEventArgs e) { - var files = (string[]) e.Data.GetData(DataFormats.FileDrop); - if (files.Any()) { - filePathTextBox.Text = files.First(); - } - } - - private void audioEventsDataGridView_CellClick(object sender, DataGridViewCellEventArgs e) { - if (e.ColumnIndex != actionColumn.Index) return; - - AudioFileModel audioFileModel = GetAudioFileModel(e.RowIndex); - audioFileModel?.PerformAction(); - } - - private void audioEventsDataGridView_CellPainting(object sender, DataGridViewCellPaintingEventArgs e) { - if (e.ColumnIndex != statusColumn.Index || e.RowIndex == -1) return; - - e.PaintBackground(e.CellBounds, false); - AudioFileModel audioFileModel = GetAudioFileModel(e.RowIndex); - if (audioFileModel == null) return; - - string text = string.Empty; - StringAlignment horizontalTextAlignment = StringAlignment.Near; - Color backgroundColor = Color.Transparent; - double? progress = null; - switch (audioFileModel.Status) { - case AudioFileStatus.NotAnimated: - break; - case AudioFileStatus.Scheduled: - text = "Waiting"; - backgroundColor = SystemColors.Highlight.WithOpacity(0.2); - break; - case AudioFileStatus.Animating: - progress = audioFileModel.AnimationProgress ?? 0.0; - text = $"{(int) (progress * 100)}%"; - horizontalTextAlignment = StringAlignment.Center; - break; - case AudioFileStatus.Done: - text = "Done"; - break; - default: - throw new ArgumentOutOfRangeException(); - } - - // Draw background - var bounds = e.CellBounds; - e.Graphics.FillRectangle(new SolidBrush(backgroundColor), bounds); - - // Draw progress bar - if (progress.HasValue) { - e.Graphics.FillRectangle( - SystemBrushes.Highlight, - bounds.X, bounds.Y, bounds.Width * (float) progress, bounds.Height); - } - - // Draw text - var stringFormat = new StringFormat { - Alignment = horizontalTextAlignment, - LineAlignment = StringAlignment.Center - }; - e.Graphics.DrawString( - text, - e.CellStyle.Font, new SolidBrush(e.CellStyle.ForeColor), - bounds, stringFormat); - - e.Handled = true; - } - - private AudioFileModel GetAudioFileModel(int rowIndex) { - return rowIndex < 0 - ? null - : MainModel.AnimationFileModel?.AudioFileModels[rowIndex]; - } - } - -} diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/MainForm.resx b/extras/rhubarb-for-spine/rhubarb-for-spine/MainForm.resx deleted file mode 100644 index 52ef75b..0000000 --- a/extras/rhubarb-for-spine/rhubarb-for-spine/MainForm.resx +++ /dev/null @@ -1,521 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 17, 17 - - - 209, 17 - - - 67 - - - - - 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 - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAASUpKEElKSkBJSkpASUpKQElKSkBJSkoQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAASUpKEElKSoBJSkrPSUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpKr0lKSmAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - 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 - Yf//lWH//5Vh//+VYf//lWH//5Vh//+VYf//glvS/5Vh//+VYf//lWH//5Vh//+VYf//lWH//5Vh//+M - Xuj/SUpK/0lKSt9JSkoQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASUpKv0lKSv+M - Xuj/lWH//5Vh//+VYf//lWH//5Vh//+VYf//lWH//5Vh//90V7D/glvS/5Vh//+VYf//lWH//5Vh//+V - Yf//lWH//5Vh//9hUYP/SUpK/0lKSv9JSkqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJ - SkpASUpK/0lKSv9XTmz/kGD0/5Vh//+VYf//lWH//5Vh//+VYf//lWH//3lYu/9hUYP/lWH//5Vh//+V - Yf//lWH//5Vh//+VYf//lWH//2ZTjv9JSkr/SUpK/0lKSv9JSkr/SUpKIAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAABJSkrfSUpK/0lKSv9JSkr/TktV/29Wpf+MXuj/lWH//5Vh//99Wsb/YVGD/0lKSv9h - UYP/jF7o/5Vh//+VYf//lWH//5Vh//+CW9L/XFB3/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpKvwAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAElKSmBJSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J - Skr/SUpK/0lKSv9JSkr/SUpK/1NNYf9cUHf/XFB3/05LVf9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J - Skr/SUpK/0lKSkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSr9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J - Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J - Skr/SUpK/0lKSv9JSkr/SUpK/0lKSq8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASUpKQElKSv9JSkr/SUpK/0lKSv9J - Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J - Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkogAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASUpKn0lKSv9J - Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J - Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkqPAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJ - SkoQSUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J - Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J - SkrfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAABJSkpgSUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J - Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J - Skr/SUpK/0lKSv9JSkr/SUpKUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkqvSUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J - Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J - Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpKnwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKShBJSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J - Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J - Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK7wAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSmBJSkr/SUpK/0lKSv9J - Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J - Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSjAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSp9J - Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/2tsbP+Njo7/pKWl/8bGxv/S - 0tL/u7u7/42Ojv/Gxsb/pKWl/42Ojv9rbGz/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J - Skr/SUpK/0lKSo8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAElKSu9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/VFVV/1RVVf/Gxsb/9PT0//////// - ////////////////////0tLS/6Slpf//////////////////////9PT0/7u7u/93d3f/SUpK/0lKSv9J - Skr/SUpK/0lKSv9JSkr/SUpK/0lKSt8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAASUpKMElKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/2BhYf+7u7v/9PT0/42Ojv// - ////////////////////////////////////0tLS/6Slpf////////////////////////////////// - ////mZmZ/3d3d/9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkogAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASUpKj0lKSv9JSkr/SUpK/0lKSv9JSkr/pKWl//////// - ////0tLS/7u7u///////////////////////////////////////0tLS/6Slpf////////////////// - ////////////////////0tLS/7u7u//S0tL/a2xs/0lKSv9JSkr/SUpK/0lKSv9JSkpwAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASUpKz0lKSv9JSkr/SUpK/2tsbP/o - 6Oj/////////////////pKWl/+jo6P//////////////////////////////////////0tLS/6Slpf// - /////////////////////////////////////////42Ojv///////////6Slpf9JSkr/SUpK/0lKSv9J - SkrPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkogSUpK/0lKSv9J - Skr/SUpK/9LS0v//////////////////////goOD///////S0tL/u7u7/6Slpf93d3f/d3d3/0lKSv9J - Skr/SUpK/0lKSv9JSkr/SUpK/3d3d/+Njo7/r7Cw/93d3f///////////42Ojv////////////////+v - sLD/SUpK/0lKSv9JSkr/SUpKIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJ - SkpwSUpK/0lKSv9JSkr/a2xs/////////////////93d3f+ZmZn/YGFh/0lKSv9JSkr/SUpK/0lKSv9J - Skq/SUpKr0lKSoBJSkqASUpKgElKSoBJSkqASUpKgElKSp9JSkq/SUpK/0lKSv9gYWH/mZmZ/4KDg//o - 6Oj/////////////////VFVV/0lKSv9JSkr/SUpKcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAABJSkrPSUpK/0lKSv9JSkr/u7u7/8bGxv+Njo7/VFVV/0lKSv9JSkr/SUpKz0lKSo9J - SkpgSUpKMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSkBJ - SkqPSUpK30lKSv9gYWH/u7u7////////////mZmZ/0lKSv9JSkr/SUpK3wAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAElKSkBJSkr/SUpK/0lKSv9JSkr/VFVV/0lKSv9JSkr/SUpKr0lKSmBJ - SkogAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAElKSlBJSkq/SUpK/2BhYf/Gxsb/3d3d/0lKSv9JSkr/SUpK/0lKSjAA - AAAAAAAAAAAAAAAAAAAAAAAAAElKSt9JSkpwAAAAAElKSp9JSkr/SUpK/0lKSv9JSkr/SUpKv0lKSmBJ - SkoQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASUpKQElKSr9JSkr/d3d3/1RVVf9J - Skr/SUpK/0lKSp8AAAAAAAAAAAAAAAAAAAAAAAAAAElKSjBJSkrPSUpK30lKSv9JSkr/SUpK/0lKSp9J - SkowAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJ - SkpQSUpK30lKSv9JSkr/SUpK/0lKSu9JSkoQAAAAAAAAAAAAAAAAAAAAAAAAAABJSkoQSUpKgElKSu9J - Skr/SUpK30lKSp9JSkqASUpKnwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAASUpKEElKSq9JSkr/SUpK/0lKSv9JSkpgAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAElKShBJSkpASUpKgElKSnBJSkpASUpKEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkpgSUpK70lKSv9JSkrfAAAAAAAAAABJ - SkpwSUpKrwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSiBJSkpASUpKcElKSu9J - Skr/SUpKr0lKSu9JSkrfSUpKMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSiBJ - SkqASUpKv0lKSr9JSkq/SUpKr0lKSmAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///////xVa////////FVr///////8VWv////// - /xVa///wP///FVr//4AP//8VWv//AgP//xVa//w/4P//FVr/+Ph4f/8VWv/xgAw//xVa//MAAj//FVr/ - 9gABn/8VWv/8AACf/xVa//gAAH//FVr/8AAAP/8VWv/gAAAf/xVa/+AAAB//FVr/wAAAD/8VWv/AAAAP - /xVa/4AAAAf/FVr/gAAAB/8VWv8AAAAD/xVa/wAAAAP/FVr+AAAAA/8VWv4AAAAB/xVa/gAAAAH/FVr8 - AAAAAf8VWvwAAAAA/xVa/AAAAAD/FVr8AAAAAP8VWvgAAAAAfxVa+AAAAAB/FVr4AAAAAH8VWvAAAAAA - PxVa8AAAAAA/FVrwAD/8AD8VWuAD//+AHxVaIB///+AfFVoA////+A8VWoB////8DxVa4H////8MFVr/ - /////gAVWv/////+AxVa////////FVr///////8VWv///////xVa////////FVr///////8VWigAAAAg - AAAAQAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkpgSUpKr0lKSr9J - Skr/SUpKv0lKSo9JSkowAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkpASUpK30lKSp9J - SkpgSUpKQElKSkBJSkpASUpKj0lKSt9JSkq/SUpKIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASUpKYElKSs9J - SkogAAAAAAAAAABJSkowSUpKQElKSiAAAAAAAAAAAElKSoBJSkrvSUpKQAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSlBJ - SkqfAAAAAElKSkBJSkqvSUpK/0lKSv9JSkr/SUpK/0lKSu9JSkqPSUpKEElKSjBJSkrfSUpKMAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAASUpKgAAAAABJSkqASUpK/1xQd/99Wsb/kGD0/5Vh//+MXuj/eVi7/1NNYf9JSkrvSUpKUElKSjBJ - SkrPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAASUpKn05LVf99Wsb/lWH//5Vh//+VYf//lWH//5Vh//+VYf//lWH//3RXsP9J - Skr/SUpKYElKSjBJSkogAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAElKSnBOS1X/h13d/5Vh//+VYf//lWH//5Vh//+VYf//lWH//5Vh//+V - Yf//lWH//3lYu/9JSkrvSUpKMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkogTktV/4dd3f+VYf//lWH//5Vh//+VYf//lWH//31axv+V - Yf//lWH//5Vh//+VYf//lWH//1NNYf9JSkrPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSr9OS1X/fVrG/5Vh//+VYf//lWH//5Vh//95 - WLv/dFew/5Vh//+VYf//lWH//5Vh//9mU47/SUpK/0lKSv9JSkpwAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkpASUpK/0lKSv9JSkr/YVGD/3lYu/90 - V7D/XFB3/0lKSv9mU47/glvS/4Jb0v95WLv/XFB3/0lKSv9JSkr/SUpK/0lKSu9JSkoQAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSr9JSkr/SUpK/0lKSv9J - Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSoAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkogSUpK/0lKSv9J - Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J - Skr/SUpK3wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSo9J - Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J - Skr/SUpK/0lKSv9JSkr/SUpKUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAASUpK30lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J - Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkqvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAElKSjBJSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J - Skr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAASUpKj0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv93 - d3f/VFVV/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSlAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkrPSUpK/0lKSv9JSkr/SUpK/0lKSv9UVVX/r7Cw/9LS0v// - //////////////+kpaX//////+jo6P+7u7v/jY6O/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpKnwAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASUpKIElKSv9JSkr/SUpK/0lKSv+Njo7/6Ojo/5mZmf// - /////////////////////////6Slpf//////////////////////xsbG/4KDg/9JSkr/SUpK/0lKSv9J - SkrvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkpwSUpK/0lKSv9rbGz/3d3d///////S - 0tL/xsbG///////////////////////S0tL/pKWl////////////////////////////pKWl/+jo6P9r - bGz/SUpK/0lKSv9JSkowAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSq9JSkr/SUpK/9LS0v// - /////////5mZmf+kpaX/jY6O/3d3d/9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv93d3f/mZmZ/93d3f+Z - mZn///////////9rbGz/SUpK/0lKSp8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkogSUpK/0lKSv9r - bGz/xsbG/42Ojv9UVVX/SUpK70lKSq9JSkqASUpKQElKSiAAAAAAAAAAAAAAAAAAAAAASUpKEElKSkBJ - SkqPSUpK30lKSv+vsLD/9PT0/6+wsP9JSkr/SUpK7wAAAAAAAAAAAAAAAAAAAABJSkpgAAAAAElKSoBJ - Skr/SUpK/0lKSv9JSkrfSUpKj0lKSkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAASUpKQElKSr9UVVX/pKWl/0lKSv9JSkr/SUpKUAAAAAAAAAAAAAAAAElKSmBJ - SkrfSUpK30lKSv9JSkq/SUpKUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSkBJSkrPSUpK/0lKSv9JSkqvAAAAAAAAAAAA - AAAAAAAAAElKSjBJSkqfSUpK30lKSs9JSkqASUpKEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkqASUpK/0lKSv9J - SkogAAAAAElKSkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElKSjBJ - SkqASUpK/0lKSr9JSkrfSUpKjwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAASUpKIElKSnBJSkqASUpKcElKSiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//////////// - //////Af///AB///jGP//yAB//9AAf//gAD//wAB//4AAf/+AAD//AAAf/wAAH/4AAB/+AAAP/gAAD/w - AAA/8AAAH/AAAB/gAAAf4AAAD+AAAA/AA8APQH/8BwP//weB///C////wP///8H///////////////8o - AAAAEAAAACAAAAABACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJ - SkqzSUpK/0lKSv9JSkr/SUpKswAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJ - SkqzAAAAAAAAAAAAAAAAAAAAAAAAAABJSkqzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAElKSv9JSkr/SUpK/0lKSv9JSkr/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAElKSv+AU6b/lWH//5Vh//+VYf//gFOm/0lKSv8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAElKSrOAU6b/lWH//5Vh//9/Uqb/lWH//5Vh//+AU6b/SUpKswAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAABJSkr/SUpK/4BTpv+AU6b/gFOm/4BTpv+AU6b/SUpK/0lKSv8AAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAABJSkqzSUpK/1lZWf9cXV3/WFlZ/1JTU/9cXV3/WFlZ/1tcXP9JSkr/SUpKswAAAAAA - AAAAAAAAAAAAAAAAAAAASUpK/0lKSv9mZ2f/amtr/29wcP90dXX/aGlp/21ubv9vcHD/SktL/0lKSv8A - AAAAAAAAAAAAAAAAAAAASUpKs0lKSv9JSkr/WFhY/1hYWP9KS0v/SktL/0pLS/9KS0v/SktL/1NUVP9J - Skr/AAAAAAAAAAAAAAAAAAAAAElKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9JSkr/SUpK/0lKSv9J - Skr/SUpK/0lKSrMAAAAAAAAAAAAAAABJSkr/SUpK/0lKSv+jpKT///////////+jpKT///////////+j - pKT/SUpK/0lKSv9JSkr/AAAAAAAAAABJSkqzSUpK/0lKSv//////o6Sk////////////o6Sk//////// - ////o6Sk//////9JSkr/SUpK/wAAAABJSkqzSUpK/0lKSv//////o6Sk/6OkpP9JSkqzSUpK/0lKSv9J - Skr/SUpK/0lKSrOjpKT//////0lKSv9JSkqzSUpK/0lKSrNJSkr/o6Sk/0lKSv9JSkqzAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAASUpKs0lKSv+jpKT/SUpK/0lKSrMAAAAASUpK/0lKSv9JSkqzAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkqzSUpK/0lKSv8AAAAAAAAAAAAAAABJSkqzSUpK/0lKSrMA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSkqzSUpK/0lKSrMAAAAAAAAAAPg/rEH336xB+D+sQfAfrEHg - D6xB4A+sQcAHrEHAB6xBgAesQYADrEGAA6xBAAKsQQAArEEH4KxBj/GsQcfjrEE= - - - - 359, 17 - - - True - - - True - - - True - - - True - - - True - - - 563, 17 - - \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/MainModel.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/MainModel.cs deleted file mode 100644 index 6c80a38..0000000 --- a/extras/rhubarb-for-spine/rhubarb-for-spine/MainModel.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.IO; -using System.Threading; - -namespace rhubarb_for_spine { - public class MainModel : ModelBase { - // For the time being, we're allowing only one file to be animated at a time. - // Rhubarb tries to use all processor cores, so there isn't much to be gained by allowing - // multiple parallel jobs. On the other hand, too many parallel Rhubarb instances may - // seriously slow down the system. - readonly SemaphoreSlim semaphore = new SemaphoreSlim(1); - - private string _filePath; - private AnimationFileModel _animationFileModel; - - public MainModel(string filePath = null) { - FilePath = filePath; - } - - public string FilePath { - get { return _filePath; } - set { - _filePath = value; - AnimationFileModel = null; - if (string.IsNullOrEmpty(_filePath)) { - SetError(nameof(FilePath), "No input file specified."); - } else if (!File.Exists(_filePath)) { - SetError(nameof(FilePath), "File does not exist."); - } else { - try { - AnimationFileModel = new AnimationFileModel(_filePath, semaphore); - SetError(nameof(FilePath), null); - } catch (Exception e) { - SetError(nameof(FilePath), e.Message); - } - } - OnPropertyChanged(nameof(FilePath)); - } - } - - public AnimationFileModel AnimationFileModel { - get { return _animationFileModel; } - set { - _animationFileModel = value; - OnPropertyChanged(nameof(AnimationFileModel)); - } - } - - } -} diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/ModelBase.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/ModelBase.cs deleted file mode 100644 index 98b30c4..0000000 --- a/extras/rhubarb-for-spine/rhubarb-for-spine/ModelBase.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel; - -namespace rhubarb_for_spine { - public class ModelBase : INotifyPropertyChanged, IDataErrorInfo { - private readonly Dictionary errors = new Dictionary(); - - public event PropertyChangedEventHandler PropertyChanged; - - protected void OnPropertyChanged(string propertyName) { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - - protected void SetError(string propertyName, string error) { - errors[propertyName] = error; - } - - string IDataErrorInfo.this[string propertyName] => - errors.ContainsKey(propertyName) ? errors[propertyName] : null; - - string IDataErrorInfo.Error => null; - - } -} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/MouthCue.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/MouthCue.cs deleted file mode 100644 index f0a062d..0000000 --- a/extras/rhubarb-for-spine/rhubarb-for-spine/MouthCue.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace rhubarb_for_spine { - public class MouthCue { - public MouthCue(TimeSpan time, MouthShape mouthShape) { - Time = time; - MouthShape = mouthShape; - } - - public TimeSpan Time { get; } - public MouthShape MouthShape { get; } - - public override string ToString() { - return $"{Time}: {MouthShape}"; - } - } -} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/MouthNaming.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/MouthNaming.cs deleted file mode 100644 index bcffaac..0000000 --- a/extras/rhubarb-for-spine/rhubarb-for-spine/MouthNaming.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace rhubarb_for_spine { - public class MouthNaming { - public MouthNaming(string prefix, string suffix, MouthShapeCasing mouthShapeCasing) { - Prefix = prefix; - Suffix = suffix; - MouthShapeCasing = mouthShapeCasing; - } - - public string Prefix { get; } - public string Suffix { get; } - public MouthShapeCasing MouthShapeCasing { get; } - - public static MouthNaming Guess(IReadOnlyCollection mouthNames) { - string firstMouthName = mouthNames.First(); - if (mouthNames.Count == 1) { - return firstMouthName == string.Empty - ? new MouthNaming(string.Empty, string.Empty, MouthShapeCasing.Lower) - : new MouthNaming( - firstMouthName.Substring(0, firstMouthName.Length - 1), - string.Empty, - GuessMouthShapeCasing(firstMouthName.Last())); - } - - string commonPrefix = mouthNames.GetCommonPrefix(); - string commonSuffix = mouthNames.GetCommonSuffix(); - var mouthShapeCasing = firstMouthName.Length > commonPrefix.Length - ? GuessMouthShapeCasing(firstMouthName[commonPrefix.Length]) - : MouthShapeCasing.Lower; - return new MouthNaming(commonPrefix, commonSuffix, mouthShapeCasing); - } - - public string DisplayString { - get { - string casing = MouthShapeCasing == MouthShapeCasing.Upper - ? "" - : ""; - return $"\"{Prefix}{casing}{Suffix}\""; - } - } - - public string GetName(MouthShape mouthShape) { - string name = MouthShapeCasing == MouthShapeCasing.Upper - ? mouthShape.ToString() - : mouthShape.ToString().ToLowerInvariant(); - return $"{Prefix}{name}{Suffix}"; - } - - private static MouthShapeCasing GuessMouthShapeCasing(char mouthShape) { - return char.IsUpper(mouthShape) ? MouthShapeCasing.Upper : MouthShapeCasing.Lower; - } - } - - public enum MouthShapeCasing { - Upper, - Lower - } -} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/MouthShape.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/MouthShape.cs deleted file mode 100644 index 0e15ee3..0000000 --- a/extras/rhubarb-for-spine/rhubarb-for-spine/MouthShape.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace rhubarb_for_spine { - public enum MouthShape { - A, B, C, D, E, F, G, H, X - } - - public static class MouthShapes { - public static IReadOnlyCollection All => - Enum.GetValues(typeof(MouthShape)) - .Cast() - .ToList(); - - public const int BasicShapesCount = 6; - - public static bool IsBasic(MouthShape mouthShape) => - (int) mouthShape < BasicShapesCount; - - public static IReadOnlyCollection Basic => - All.Take(BasicShapesCount).ToList(); - } -} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/ProcessTools.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/ProcessTools.cs deleted file mode 100644 index 7e3a135..0000000 --- a/extras/rhubarb-for-spine/rhubarb-for-spine/ProcessTools.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -using Nito.AsyncEx; - -namespace rhubarb_for_spine { - public static class ProcessTools { - - public static async Task RunProcessAsync( - string processFilePath, - string processArgs, - Action receiveStdout, - Action receiveStderr, - CancellationToken cancellationToken - ) { - var startInfo = new ProcessStartInfo { - FileName = processFilePath, - Arguments = processArgs, - UseShellExecute = false, // Necessary to redirect streams - CreateNoWindow = true, - RedirectStandardOutput = true, - RedirectStandardError = true - }; - using (var process = new Process { StartInfo = startInfo, EnableRaisingEvents = true }) { - // Process all events in the original call's context - var pendingActions = new AsyncProducerConsumerQueue(); - int? exitCode = null; - - // ReSharper disable once AccessToDisposedClosure - process.Exited += (sender, args) => - pendingActions.Enqueue(() => exitCode = process.ExitCode); - - process.OutputDataReceived += (sender, args) => { - if (args.Data != null) { - pendingActions.Enqueue(() => receiveStdout(args.Data)); - } - }; - process.ErrorDataReceived += (sender, args) => { - if (args.Data != null) { - pendingActions.Enqueue(() => receiveStderr(args.Data)); - } - }; - - process.Start(); - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - - cancellationToken.Register(() => pendingActions.Enqueue(() => { - try { - // ReSharper disable once AccessToDisposedClosure - process.Kill(); - } catch (Exception e) { - Debug.WriteLine($"Error terminating process: {e}"); - } - // ReSharper disable once AccessToDisposedClosure - process.WaitForExit(); - throw new OperationCanceledException(); - })); - - while (exitCode == null) { - Action action = await pendingActions.DequeueAsync(cancellationToken); - action(); - } - return exitCode.Value; - } - } - - } -} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/Program.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/Program.cs deleted file mode 100644 index dc1e8e3..0000000 --- a/extras/rhubarb-for-spine/rhubarb-for-spine/Program.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Linq; -using System.Windows.Forms; - -namespace rhubarb_for_spine { - static class Program { - [STAThread] - static void Main(string[] args) { - Application.EnableVisualStyles(); - Application.SetCompatibleTextRenderingDefault(false); - string filePath = args.FirstOrDefault(); - MainModel mainModel = new MainModel(filePath); - Application.Run(new MainForm(mainModel)); - } - } -} diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/Properties/AssemblyInfo.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/Properties/AssemblyInfo.cs deleted file mode 100644 index 795df04..0000000 --- a/extras/rhubarb-for-spine/rhubarb-for-spine/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("rhubarb-for-spine")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("rhubarb-for-spine")] -[assembly: AssemblyCopyright("Copyright © 2017")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("c5ed6f8a-6141-4bae-be24-77dee23e495f")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/Properties/DataSources/MainModel.datasource b/extras/rhubarb-for-spine/rhubarb-for-spine/Properties/DataSources/MainModel.datasource deleted file mode 100644 index d8a8c22..0000000 --- a/extras/rhubarb-for-spine/rhubarb-for-spine/Properties/DataSources/MainModel.datasource +++ /dev/null @@ -1,10 +0,0 @@ - - - - rhubarb_for_spine.MainModel, rhubarb-for-spine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null - \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/Properties/Settings.Designer.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/Properties/Settings.Designer.cs deleted file mode 100644 index b80fcd7..0000000 --- a/extras/rhubarb-for-spine/rhubarb-for-spine/Properties/Settings.Designer.cs +++ /dev/null @@ -1,26 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace rhubarb_for_spine.Properties { - - - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { - - private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default { - get { - return defaultInstance; - } - } - } -} diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/Properties/Settings.settings b/extras/rhubarb-for-spine/rhubarb-for-spine/Properties/Settings.settings deleted file mode 100644 index 3964565..0000000 --- a/extras/rhubarb-for-spine/rhubarb-for-spine/Properties/Settings.settings +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/RhubarbCli.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/RhubarbCli.cs deleted file mode 100644 index 6e1e4c6..0000000 --- a/extras/rhubarb-for-spine/rhubarb-for-spine/RhubarbCli.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using Newtonsoft.Json.Linq; - -namespace rhubarb_for_spine { - public static class RhubarbCli { - public static async Task> AnimateAsync( - string audioFilePath, string dialog, ISet extendedMouthShapes, - IProgress progress, CancellationToken cancellationToken - ) { - if (cancellationToken.IsCancellationRequested) { - throw new OperationCanceledException(); - } - if (!File.Exists(audioFilePath)) { - throw new ArgumentException($"File '{audioFilePath}' does not exist."); - } - - using (var dialogFile = dialog != null ? new TemporaryTextFile(dialog) : null) { - string rhubarbExePath = GetRhubarbExePath(); - string args = CreateArgs(audioFilePath, extendedMouthShapes, dialogFile?.FilePath); - - bool success = false; - string errorMessage = null; - string resultString = ""; - - await ProcessTools.RunProcessAsync( - rhubarbExePath, args, - outString => resultString += outString, - errString => { - dynamic json = JObject.Parse(errString); - switch ((string) json.type) { - case "progress": - progress.Report((double) json.value); - break; - case "success": - success = true; - break; - case "failure": - errorMessage = json.reason; - break; - } - }, - cancellationToken); - - if (errorMessage != null) { - throw new ApplicationException(errorMessage); - } - if (success) { - progress.Report(1.0); - return ParseRhubarbResult(resultString); - } - throw new ApplicationException("Rhubarb did not return a result."); - } - } - - private static string CreateArgs( - string audioFilePath, ISet extendedMouthShapes, string dialogFilePath - ) { - string extendedShapesString = - string.Join("", extendedMouthShapes.Select(shape => shape.ToString())); - string args = "--machineReadable" - + " --exportFormat json" - + $" --extendedShapes \"{extendedShapesString}\"" - + $" \"{audioFilePath}\""; - if (dialogFilePath != null) { - args = $"--dialogFile \"{dialogFilePath}\" " + args; - } - return args; - } - - private static IReadOnlyCollection ParseRhubarbResult(string jsonString) { - dynamic json = JObject.Parse(jsonString); - JArray mouthCues = json.mouthCues; - return mouthCues - .Cast() - .Select(mouthCue => { - TimeSpan time = TimeSpan.FromSeconds((double) mouthCue.start); - MouthShape mouthShape = (MouthShape) Enum.Parse(typeof(MouthShape), (string) mouthCue.value); - return new MouthCue(time, mouthShape); - }) - .ToList(); - } - - private static string GetRhubarbExePath() { - bool onUnix = Environment.OSVersion.Platform == PlatformID.Unix - || Environment.OSVersion.Platform == PlatformID.MacOSX; - string exeName = "rhubarb" + (onUnix ? "" : ".exe"); - string guiExeDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - string currentDirectory = guiExeDirectory; - while (currentDirectory != Path.GetPathRoot(guiExeDirectory)) { - string candidate = Path.Combine(currentDirectory, exeName); - if (File.Exists(candidate)) { - return candidate; - } - currentDirectory = Path.GetDirectoryName(currentDirectory); - } - throw new ApplicationException( - $"Could not find Rhubarb Lip Sync executable '{exeName}'." - + $" Expected to find it in '{guiExeDirectory}' or any directory above."); - } - - private class TemporaryTextFile : IDisposable { - public string FilePath { get; } - - public TemporaryTextFile(string text) { - FilePath = Path.GetTempFileName(); - File.WriteAllText(FilePath, text); - } - - public void Dispose() { - File.Delete(FilePath); - } - } - } -} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/SpineJson.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/SpineJson.cs deleted file mode 100644 index dcf1024..0000000 --- a/extras/rhubarb-for-spine/rhubarb-for-spine/SpineJson.cs +++ /dev/null @@ -1,156 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Newtonsoft.Json.Linq; - -namespace rhubarb_for_spine { - public static class SpineJson { - public static JObject ReadJson(string filePath) { - string jsonString = File.ReadAllText(filePath); - try { - return JObject.Parse(jsonString); - } catch (Exception) { - throw new ApplicationException("Wrong file format. This is not a valid JSON file."); - } - } - - public static void ValidateJson(JObject json, string animationFileDirectory) { - // This method doesn't validate the entire JSON. - // It merely checks that there are no obvious problems. - - dynamic skeleton = ((dynamic) json).skeleton; - if (skeleton == null) { - throw new ApplicationException("JSON file is corrupted."); - } - if (skeleton.images == null) { - throw new ApplicationException( - "JSON file is incomplete. Make sure you checked 'Nonessential data' when exporting."); - } - if (((dynamic) json).skins["default"] == null) { - throw new ApplicationException("JSON file has no default skin."); - } - GetImagesDirectory(json, animationFileDirectory); - GetAudioDirectory(json, animationFileDirectory); - } - - public static string GetImagesDirectory(JObject json, string animationFileDirectory) { - string result = Path.GetFullPath(Path.Combine(animationFileDirectory, (string) ((dynamic) json).skeleton.images)); - if (!Directory.Exists(result)) { - throw new ApplicationException( - "Could not find images directory relative to the JSON file." - + " Make sure the JSON file is in the same directory as the original Spine file."); - } - return result; - } - - public static string GetAudioDirectory(JObject json, string animationFileDirectory) { - string result = Path.GetFullPath(Path.Combine(animationFileDirectory, (string) ((dynamic) json).skeleton.audio)); - if (!Directory.Exists(result)) { - throw new ApplicationException( - "Could not find audio directory relative to the JSON file." - + " Make sure the JSON file is in the same directory as the original Spine file."); - } - return result; - } - - public static double GetFrameRate(JObject json) { - return (double?) ((dynamic) json).skeleton.fps ?? 30.0; - } - - public static IReadOnlyCollection GetSlots(JObject json) { - return ((JArray) ((dynamic) json).slots) - .Cast() - .Select(slot => (string) slot.name) - .ToList(); - } - - public static string GuessMouthSlot(IReadOnlyCollection slots) { - return slots.FirstOrDefault(slot => slot.Contains("mouth", StringComparison.InvariantCultureIgnoreCase)) - ?? slots.FirstOrDefault(); - } - - public class AudioEvent { - public AudioEvent(string name, string relativeAudioFilePath, string dialog) { - Name = name; - RelativeAudioFilePath = relativeAudioFilePath; - Dialog = dialog; - } - - public string Name { get; } - public string RelativeAudioFilePath { get; } - public string Dialog { get; } - } - - public static IReadOnlyCollection GetAudioEvents(JObject json) { - return ((IEnumerable>) ((dynamic) json).events) - .Select(pair => { - string name = pair.Key; - dynamic value = pair.Value; - string relativeAudioFilePath = value.audio; - string dialog = value["string"]; - return new AudioEvent(name, relativeAudioFilePath, dialog); - }) - .Where(audioEvent => audioEvent.RelativeAudioFilePath != null) - .ToList(); - } - - public static IReadOnlyCollection GetSlotAttachmentNames(JObject json, string slotName) { - return ((JObject) ((dynamic) json).skins["default"][slotName]) - .Properties() - .Select(property => property.Name) - .ToList(); - } - - public static bool HasAnimation(JObject json, string animationName) { - JObject animations = ((dynamic) json).animations; - if (animations == null) return false; - - return animations.Properties().Any(property => property.Name == animationName); - } - - public static void CreateOrUpdateAnimation( - JObject json, IReadOnlyCollection animationResult, - string eventName, string animationName, string mouthSlot, MouthNaming mouthNaming - ) { - dynamic dynamicJson = json; - dynamic animations = dynamicJson.animations; - if (animations == null) { - animations = dynamicJson.animations = new JObject(); - } - - // Round times to full frame. Always round down. - // If events coincide, prefer the later one. - double frameRate = GetFrameRate(json); - var keyframes = new Dictionary(); - foreach (MouthCue mouthCue in animationResult) { - int frameNumber = (int) (mouthCue.Time.TotalSeconds * frameRate); - keyframes[frameNumber] = mouthCue.MouthShape; - } - - animations[animationName] = new JObject { - ["slots"] = new JObject { - [mouthSlot] = new JObject { - ["attachment"] = new JArray( - keyframes - .OrderBy(pair => pair.Key) - .Select(pair => new JObject { - ["time"] = pair.Key / frameRate, - ["name"] = mouthNaming.GetName(pair.Value) - }) - .Cast() - .ToArray() - ) - } - }, - ["events"] = new JArray { - new JObject { - ["time"] = 0.0, - ["name"] = eventName, - ["string"] = "" - } - } - }; - } - } -} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/Tools.cs b/extras/rhubarb-for-spine/rhubarb-for-spine/Tools.cs deleted file mode 100644 index 3540d7e..0000000 --- a/extras/rhubarb-for-spine/rhubarb-for-spine/Tools.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; - -namespace rhubarb_for_spine { - public static class Tools { - public static string GetCommonPrefix(this IReadOnlyCollection strings) { - return strings.Any() - ? strings.First().Substring(0, GetCommonPrefixLength(strings)) - : string.Empty; - } - - public static int GetCommonPrefixLength(this IReadOnlyCollection strings) { - if (!strings.Any()) return 0; - - string first = strings.First(); - int result = first.Length; - foreach (string s in strings) { - for (int i = 0; i < Math.Min(result, s.Length); i++) { - if (s[i] != first[i]) { - result = i; - break; - } - } - } - return result; - } - - public static string GetCommonSuffix(this IReadOnlyCollection strings) { - if (!strings.Any()) return string.Empty; - - int commonSuffixLength = GetCommonSuffixLength(strings); - string first = strings.First(); - return first.Substring(first.Length - commonSuffixLength); - } - - public static int GetCommonSuffixLength(this IReadOnlyCollection strings) { - if (!strings.Any()) return 0; - - string first = strings.First(); - int result = first.Length; - foreach (string s in strings) { - for (int i = 0; i < Math.Min(result, s.Length); i++) { - if (s[s.Length - 1 - i] != first[first.Length - 1 - i]) { - result = i; - break; - } - } - } - return result; - } - - public static bool Contains(this string s, string value, StringComparison stringComparison) { - return s.IndexOf(value, stringComparison) >= 0; - } - - public static string Join(this IEnumerable values, string separator) { - return string.Join(separator, values); - } - - public static Color WithOpacity(this Color color, double opacity) { - return Color.FromArgb((int) (opacity * 255), color); - } - } - } \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/app.config b/extras/rhubarb-for-spine/rhubarb-for-spine/app.config deleted file mode 100644 index b45f31e..0000000 --- a/extras/rhubarb-for-spine/rhubarb-for-spine/app.config +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/packages.config b/extras/rhubarb-for-spine/rhubarb-for-spine/packages.config deleted file mode 100644 index f2cad59..0000000 --- a/extras/rhubarb-for-spine/rhubarb-for-spine/packages.config +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/rhubarb-for-spine.csproj b/extras/rhubarb-for-spine/rhubarb-for-spine/rhubarb-for-spine.csproj deleted file mode 100644 index ad62196..0000000 --- a/extras/rhubarb-for-spine/rhubarb-for-spine/rhubarb-for-spine.csproj +++ /dev/null @@ -1,138 +0,0 @@ - - - - - Debug - AnyCPU - {C5ED6F8A-6141-4BAE-BE24-77DEE23E495F} - WinExe - Properties - rhubarb_for_spine - rhubarb-for-spine - v4.6 - 512 - - - - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - false - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - false - - - rhubarb.ico - - - - - ..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.dll - True - - - ..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.dll - True - - - ..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.Desktop.dll - True - - - ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll - True - - - ..\packages\Nito.AsyncEx.4.0.1\lib\net45\Nito.AsyncEx.dll - True - - - ..\packages\Nito.AsyncEx.4.0.1\lib\net45\Nito.AsyncEx.Concurrent.dll - True - - - ..\packages\Nito.AsyncEx.4.0.1\lib\net45\Nito.AsyncEx.Enlightenment.dll - True - - - - - - - - - - - - - - - Component - - - Form - - - MainForm.cs - - - - - - - - - - - - - - MainForm.cs - - - - - - SettingsSingleFileGenerator - Settings.Designer.cs - - - True - Settings.settings - True - - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - \ No newline at end of file diff --git a/extras/rhubarb-for-spine/rhubarb-for-spine/rhubarb.ico b/extras/rhubarb-for-spine/rhubarb-for-spine/rhubarb.ico deleted file mode 100644 index 0e9923f42e7e91f50221ea9229b59696514d9fb3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21947 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 From d46e574b8ec71a9443ab7b3243733c75926231c8 Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Tue, 14 Nov 2017 20:41:42 +0100 Subject: [PATCH 05/33] Created empty rhubarb-for-spine project using Kotlin/JavaFX I hope that's a better tech stack. --- extras/rhubarb-for-spine/.gitignore | 4 + extras/rhubarb-for-spine/build.gradle | 30 +++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54788 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + extras/rhubarb-for-spine/gradlew | 172 ++++++++++++++++++ extras/rhubarb-for-spine/gradlew.bat | 84 +++++++++ extras/rhubarb-for-spine/settings.gradle | 2 + 7 files changed, 298 insertions(+) create mode 100644 extras/rhubarb-for-spine/.gitignore create mode 100644 extras/rhubarb-for-spine/build.gradle create mode 100644 extras/rhubarb-for-spine/gradle/wrapper/gradle-wrapper.jar create mode 100644 extras/rhubarb-for-spine/gradle/wrapper/gradle-wrapper.properties create mode 100644 extras/rhubarb-for-spine/gradlew create mode 100644 extras/rhubarb-for-spine/gradlew.bat create mode 100644 extras/rhubarb-for-spine/settings.gradle diff --git a/extras/rhubarb-for-spine/.gitignore b/extras/rhubarb-for-spine/.gitignore new file mode 100644 index 0000000..e02cdcb --- /dev/null +++ b/extras/rhubarb-for-spine/.gitignore @@ -0,0 +1,4 @@ +/.gradle/ +/.idea/workspace.xml +/.idea/tasks.xml +/build/ diff --git a/extras/rhubarb-for-spine/build.gradle b/extras/rhubarb-for-spine/build.gradle new file mode 100644 index 0000000..d3ea723 --- /dev/null +++ b/extras/rhubarb-for-spine/build.gradle @@ -0,0 +1,30 @@ +group 'com.rhubarb_lip_sync' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.1.4-3' + + repositories { + mavenCentral() + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +apply plugin: 'kotlin' + +repositories { + mavenCentral() +} + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" +} + +compileKotlin { + kotlinOptions.jvmTarget = "1.8" +} +compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" +} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/gradle/wrapper/gradle-wrapper.jar b/extras/rhubarb-for-spine/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..5c37b441829ca7790c5885480c6c4c424602c39f GIT binary patch literal 54788 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/extras/rhubarb-for-spine/gradlew.bat b/extras/rhubarb-for-spine/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/extras/rhubarb-for-spine/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/extras/rhubarb-for-spine/settings.gradle b/extras/rhubarb-for-spine/settings.gradle new file mode 100644 index 0000000..dc991a3 --- /dev/null +++ b/extras/rhubarb-for-spine/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'rhubarb-for-spine' + From 82c0a1531e651c96a8397307617dad49a48690d8 Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Thu, 16 Nov 2017 21:46:06 +0100 Subject: [PATCH 06/33] Dynamically reading version from CMake file --- extras/rhubarb-for-spine/build.gradle | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/extras/rhubarb-for-spine/build.gradle b/extras/rhubarb-for-spine/build.gradle index d3ea723..8b6f299 100644 --- a/extras/rhubarb-for-spine/build.gradle +++ b/extras/rhubarb-for-spine/build.gradle @@ -1,5 +1,16 @@ +def getVersion() { + // Dynamically read version from CMake file + String text = new File('../../appInfo.cmake').getText('UTF-8') + String major = (text =~ /appVersionMajor\s+(\d+)/)[0][1] + String minor = (text =~ /appVersionMinor\s+(\d+)/)[0][1] + String patch = (text =~ /appVersionPatch\s+(\d+)/)[0][1] + String suffix = (text =~ /appVersionSuffix\s+"(.*?)"/)[0][1] + String result = "${major}.${minor}.${patch}${suffix}" + return result +} + group 'com.rhubarb_lip_sync' -version '1.0-SNAPSHOT' +version = getVersion() buildscript { ext.kotlin_version = '1.1.4-3' From 4d9ddf334f3aebf5baf4365653ce60d268c58534 Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Tue, 14 Nov 2017 21:21:05 +0100 Subject: [PATCH 07/33] Loading Spine JSON file --- .gitignore | 1 - extras/rhubarb-for-spine/.gitignore | 13 +- .../.idea/codeStyleSettings.xml | 9 + extras/rhubarb-for-spine/.idea/compiler.xml | 9 + extras/rhubarb-for-spine/.idea/kotlinc.xml | 7 + extras/rhubarb-for-spine/.idea/misc.xml | 6 + extras/rhubarb-for-spine/.idea/modules.xml | 10 + extras/rhubarb-for-spine/.idea/vcs.xml | 6 + extras/rhubarb-for-spine/build.gradle | 26 +-- .../gradle/wrapper/gradle-wrapper.properties | 4 +- .../rhubarb_for_spine/AnimationFileModel.kt | 11 ++ .../rhubarb_for_spine/ErrorProperty.kt | 80 ++++++++ .../rhubarb_for_spine/MainApp.kt | 5 + .../rhubarb_for_spine/MainModel.kt | 50 +++++ .../rhubarb_for_spine/MainView.kt | 77 ++++++++ .../rhubarb_for_spine/MouthCue.kt | 3 + .../rhubarb_for_spine/MouthNaming.kt | 48 +++++ .../rhubarb_for_spine/MouthShape.kt | 5 + .../rhubarb_for_spine/Progress.kt | 6 + .../rhubarb_for_spine/RhubarbTask.kt | 173 ++++++++++++++++++ .../rhubarb_for_spine/SpineJson.kt | 141 ++++++++++++++ .../rhubarb_for_spine/main.kt | 7 + .../rhubarb_for_spine/tools.kt | 23 +++ 23 files changed, 704 insertions(+), 16 deletions(-) create mode 100644 extras/rhubarb-for-spine/.idea/codeStyleSettings.xml create mode 100644 extras/rhubarb-for-spine/.idea/compiler.xml create mode 100644 extras/rhubarb-for-spine/.idea/kotlinc.xml create mode 100644 extras/rhubarb-for-spine/.idea/misc.xml create mode 100644 extras/rhubarb-for-spine/.idea/modules.xml create mode 100644 extras/rhubarb-for-spine/.idea/vcs.xml create mode 100644 extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt create mode 100644 extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/ErrorProperty.kt create mode 100644 extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainApp.kt create mode 100644 extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainModel.kt create mode 100644 extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt create mode 100644 extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MouthCue.kt create mode 100644 extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MouthNaming.kt create mode 100644 extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MouthShape.kt create mode 100644 extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/Progress.kt create mode 100644 extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/RhubarbTask.kt create mode 100644 extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/SpineJson.kt create mode 100644 extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/main.kt create mode 100644 extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/tools.kt diff --git a/.gitignore b/.gitignore index 717f29f..3b6a86f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -.idea/ .vs/ build/ *.user \ No newline at end of file diff --git a/extras/rhubarb-for-spine/.gitignore b/extras/rhubarb-for-spine/.gitignore index e02cdcb..5889e76 100644 --- a/extras/rhubarb-for-spine/.gitignore +++ b/extras/rhubarb-for-spine/.gitignore @@ -1,4 +1,13 @@ +# User-specific files: +/.idea/**/workspace.xml +/.idea/**/tasks.xml +/.idea/dictionaries/ + +# Gradle: /.gradle/ -/.idea/workspace.xml -/.idea/tasks.xml +/.idea/**/gradle.xml +/.idea/**/libraries/ +/.idea/**/*.iml + /build/ +/out/ \ No newline at end of file diff --git a/extras/rhubarb-for-spine/.idea/codeStyleSettings.xml b/extras/rhubarb-for-spine/.idea/codeStyleSettings.xml new file mode 100644 index 0000000..5555dd2 --- /dev/null +++ b/extras/rhubarb-for-spine/.idea/codeStyleSettings.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/extras/rhubarb-for-spine/.idea/compiler.xml b/extras/rhubarb-for-spine/.idea/compiler.xml new file mode 100644 index 0000000..34ed3d3 --- /dev/null +++ b/extras/rhubarb-for-spine/.idea/compiler.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/extras/rhubarb-for-spine/.idea/kotlinc.xml b/extras/rhubarb-for-spine/.idea/kotlinc.xml new file mode 100644 index 0000000..5806fb3 --- /dev/null +++ b/extras/rhubarb-for-spine/.idea/kotlinc.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/extras/rhubarb-for-spine/.idea/misc.xml b/extras/rhubarb-for-spine/.idea/misc.xml new file mode 100644 index 0000000..e208459 --- /dev/null +++ b/extras/rhubarb-for-spine/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/extras/rhubarb-for-spine/.idea/modules.xml b/extras/rhubarb-for-spine/.idea/modules.xml new file mode 100644 index 0000000..58be9c9 --- /dev/null +++ b/extras/rhubarb-for-spine/.idea/modules.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/extras/rhubarb-for-spine/.idea/vcs.xml b/extras/rhubarb-for-spine/.idea/vcs.xml new file mode 100644 index 0000000..b2bdec2 --- /dev/null +++ b/extras/rhubarb-for-spine/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/extras/rhubarb-for-spine/build.gradle b/extras/rhubarb-for-spine/build.gradle index 8b6f299..d39c4cf 100644 --- a/extras/rhubarb-for-spine/build.gradle +++ b/extras/rhubarb-for-spine/build.gradle @@ -13,29 +13,33 @@ group 'com.rhubarb_lip_sync' version = getVersion() buildscript { - ext.kotlin_version = '1.1.4-3' + ext.kotlin_version = '1.1.60' - repositories { - mavenCentral() - } - dependencies { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } + repositories { + mavenCentral() + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } } apply plugin: 'kotlin' repositories { - mavenCentral() + mavenCentral() + jcenter() } dependencies { - compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + compile 'com.beust:klaxon:0.30' + compile 'org.apache.commons:commons-lang3:3.7' + compile 'no.tornado:tornadofx:1.7.12' } compileKotlin { - kotlinOptions.jvmTarget = "1.8" + kotlinOptions.jvmTarget = '1.8' } compileTestKotlin { - kotlinOptions.jvmTarget = "1.8" + kotlinOptions.jvmTarget = '1.8' } \ No newline at end of file diff --git a/extras/rhubarb-for-spine/gradle/wrapper/gradle-wrapper.properties b/extras/rhubarb-for-spine/gradle/wrapper/gradle-wrapper.properties index c50fb35..22c733c 100644 --- a/extras/rhubarb-for-spine/gradle/wrapper/gradle-wrapper.properties +++ b/extras/rhubarb-for-spine/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Nov 14 20:30:34 CET 2017 +#Tue Nov 28 18:16:46 CET 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-rc-2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-rc-2-all.zip diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt new file mode 100644 index 0000000..1c1a1d0 --- /dev/null +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt @@ -0,0 +1,11 @@ +package com.rhubarb_lip_sync.rhubarb_for_spine + +import java.nio.file.Path + +class AnimationFileModel(animationFilePath: Path) { + val spineJson: SpineJson + + init { + spineJson = SpineJson(animationFilePath) + } +} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/ErrorProperty.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/ErrorProperty.kt new file mode 100644 index 0000000..3f20a63 --- /dev/null +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/ErrorProperty.kt @@ -0,0 +1,80 @@ +package com.rhubarb_lip_sync.rhubarb_for_spine + +import javafx.beans.property.SimpleStringProperty +import javafx.beans.property.StringProperty +import javafx.beans.value.ObservableValue +import javafx.scene.Group +import javafx.scene.Node +import javafx.scene.Parent +import javafx.scene.control.Tooltip +import javafx.scene.paint.Color +import tornadofx.addChildIfPossible +import tornadofx.circle +import tornadofx.rectangle +import tornadofx.removeFromParent + +fun renderErrorIndicator(): Node { + return Group().apply { + isManaged = false + circle { + radius = 7.0 + fill = Color.ORANGERED + } + rectangle { + x = -1.0 + y = -5.0 + width = 2.0 + height = 7.0 + fill = Color.WHITE + } + rectangle { + x = -1.0 + y = 3.0 + width = 2.0 + height = 2.0 + fill = Color.WHITE + } + } +} + +fun Parent.errorProperty() : StringProperty { + return properties.getOrPut("rhubarb.errorProperty", { + val errorIndicator: Node = renderErrorIndicator() + val tooltip = Tooltip() + val property = SimpleStringProperty() + + fun updateTooltipVisibility() { + if (tooltip.text.isNotEmpty() && isFocused) { + val bounds = localToScreen(boundsInLocal) + tooltip.show(scene.window, bounds.minX + 5, bounds.maxY + 2) + } else { + tooltip.hide() + } + } + + focusedProperty().addListener({ + _: ObservableValue, _: Boolean, _: Boolean -> + updateTooltipVisibility() + }) + + property.addListener({ + _: ObservableValue, _: String?, newValue: String? -> + + if (newValue != null) { + this.addChildIfPossible(errorIndicator) + + tooltip.text = newValue + Tooltip.install(this, tooltip) + updateTooltipVisibility() + } else { + errorIndicator.removeFromParent() + + tooltip.text = "" + tooltip.hide() + Tooltip.uninstall(this, tooltip) + updateTooltipVisibility() + } + }) + return@getOrPut property + }) as StringProperty +} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainApp.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainApp.kt new file mode 100644 index 0000000..0d21765 --- /dev/null +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainApp.kt @@ -0,0 +1,5 @@ +package com.rhubarb_lip_sync.rhubarb_for_spine + +import tornadofx.App + +class MainApp : App(MainView::class) diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainModel.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainModel.kt new file mode 100644 index 0000000..ca3dcb2 --- /dev/null +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainModel.kt @@ -0,0 +1,50 @@ +package com.rhubarb_lip_sync.rhubarb_for_spine + +import javafx.beans.property.SimpleObjectProperty +import javafx.beans.property.SimpleStringProperty +import tornadofx.FX +import tornadofx.getValue +import tornadofx.setValue +import java.nio.file.Files +import java.nio.file.InvalidPathException +import java.nio.file.Paths + +class MainModel { + val filePathStringProperty = SimpleStringProperty(getDefaultPathString()).applyListener { value -> + try { + animationFileModel = null + if (value.isNullOrBlank()) { + throw Exception("No input file specified.") + } + + val path = try { + val trimmed = value.removeSurrounding("\"") + Paths.get(trimmed) + } catch (e: InvalidPathException) { + throw Exception("Not a valid file path.") + } + + if (!Files.exists(path)) { + throw Exception("File does not exist.") + } + + animationFileModel = AnimationFileModel(path) + + filePathError = null + } catch (e: Exception) { + filePathError = e.message + } + } + + var filePathString by filePathStringProperty + + val filePathErrorProperty = SimpleStringProperty() + var filePathError by filePathErrorProperty + private set + + val animationFileModelProperty = SimpleObjectProperty() + var animationFileModel by animationFileModelProperty + private set + + private fun getDefaultPathString() = FX.application.parameters.raw.firstOrNull() +} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt new file mode 100644 index 0000000..85d5b72 --- /dev/null +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt @@ -0,0 +1,77 @@ +package com.rhubarb_lip_sync.rhubarb_for_spine + +import javafx.event.EventHandler +import javafx.scene.control.TextField +import javafx.scene.input.DragEvent +import javafx.scene.input.TransferMode +import tornadofx.* +import java.time.LocalDate +import java.time.Period + +class MainView : View() { + + val mainModel = MainModel() + + class Person(val id: Int, val name: String, val birthday: LocalDate) { + val age: Int get() = Period.between(birthday, LocalDate.now()).years + } + + private val persons = listOf( + Person(1,"Samantha Stuart",LocalDate.of(1981,12,4)), + Person(2,"Tom Marks",LocalDate.of(2001,1,23)), + Person(3,"Stuart Gills",LocalDate.of(1989,5,23)), + Person(3,"Nicole Williams",LocalDate.of(1998,8,11)) + ).observable() + + init { + title = "Rhubarb Lip Sync for Spine" + } + + override val root = form { + var filePathField: TextField? = null + + minWidth = 800.0 + fieldset("Settings") { + field("Spine JSON file") { + filePathField = textfield { + textProperty().bindBidirectional(mainModel.filePathStringProperty) + tooltip("Hello world") + errorProperty().bind(mainModel.filePathErrorProperty) + } + button("...") + } + field("Mouth slot") { + textfield() + } + field("Mouth naming") { + datepicker() + } + field("Mouth shapes") { + textfield() + } + } + fieldset("Audio events") { + tableview(persons) { + column("Event", Person::id) + column("Audio file", Person::name) + column("Dialog", Person::birthday) + column("Status", Person::age) + column("", Person::age) + } + } + + onDragOver = EventHandler { event -> + if (event.dragboard.hasFiles()) { + event.acceptTransferModes(TransferMode.COPY) + event.consume() + } + } + onDragDropped = EventHandler { event -> + if (event.dragboard.hasFiles()) { + filePathField!!.text = event.dragboard.files.firstOrNull()?.path + event.isDropCompleted = true + event.consume() + } + } + } +} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MouthCue.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MouthCue.kt new file mode 100644 index 0000000..4e85edf --- /dev/null +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MouthCue.kt @@ -0,0 +1,3 @@ +package com.rhubarb_lip_sync.rhubarb_for_spine + +data class MouthCue(val time: Double, val mouthShape: MouthShape) diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MouthNaming.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MouthNaming.kt new file mode 100644 index 0000000..25dbc21 --- /dev/null +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MouthNaming.kt @@ -0,0 +1,48 @@ +package com.rhubarb_lip_sync.rhubarb_for_spine + +import java.util.* + +class MouthNaming(val prefix: String, val suffix: String, val mouthShapeCasing: MouthShapeCasing) { + + companion object { + fun guess(mouthNames: List): MouthNaming { + if (mouthNames.isEmpty()) return MouthNaming("", "", guessMouthShapeCasing("")) + + val commonPrefix = mouthNames.commonPrefix + val commonSuffix = mouthNames.commonSuffix + val shapeName = mouthNames.first().substring( + commonPrefix.length, + mouthNames.first().length - commonSuffix.length) + val mouthShapeCasing = guessMouthShapeCasing(shapeName) + return MouthNaming(commonPrefix, commonSuffix, mouthShapeCasing) + } + + private fun guessMouthShapeCasing(shapeName: String): MouthShapeCasing { + return if (shapeName.isBlank() || shapeName[0].isLowerCase()) + MouthShapeCasing.Lower + else + MouthShapeCasing.Upper + } + } + + fun getName(mouthShape: MouthShape): String { + val name = if (mouthShapeCasing == MouthShapeCasing.Upper) + mouthShape.toString() + else + mouthShape.toString().toLowerCase(Locale.ROOT) + return "$prefix$name$suffix" + } + + val displayString: String get() { + val casing = if (mouthShapeCasing == MouthShapeCasing.Upper) + "" + else + "" + return "\"$prefix$casing$suffix\"" + } +} + +enum class MouthShapeCasing { + Upper, + Lower +} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MouthShape.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MouthShape.kt new file mode 100644 index 0000000..5f27ec0 --- /dev/null +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MouthShape.kt @@ -0,0 +1,5 @@ +package com.rhubarb_lip_sync.rhubarb_for_spine + +enum class MouthShape { + A, B, C, D, E, F, G, H, X +} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/Progress.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/Progress.kt new file mode 100644 index 0000000..d8ce1bf --- /dev/null +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/Progress.kt @@ -0,0 +1,6 @@ +package com.rhubarb_lip_sync.rhubarb_for_spine + +// Modeled after C#'s IProgress +interface Progress { + fun reportProgress(progress: Double) +} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/RhubarbTask.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/RhubarbTask.kt new file mode 100644 index 0000000..24ede9f --- /dev/null +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/RhubarbTask.kt @@ -0,0 +1,173 @@ +package com.rhubarb_lip_sync.rhubarb_for_spine + +import com.beust.klaxon.JsonObject +import com.beust.klaxon.array +import com.beust.klaxon.double +import com.beust.klaxon.string +import com.beust.klaxon.Parser as JsonParser +import org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS +import java.io.BufferedReader +import java.io.EOFException +import java.io.InputStreamReader +import java.io.StringReader +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.time.Duration +import java.util.concurrent.Callable + +class RhubarbTask( + val audioFilePath: Path, + val dialog: String?, + val extendedMouthShapes: Set, + val progress: Progress +) : Callable> { + + override fun call(): List { + if (Thread.currentThread().isInterrupted) { + throw InterruptedException() + } + if (!Files.exists(audioFilePath)) { + throw IllegalArgumentException("File '$audioFilePath' does not exist."); + } + + + val dialogFile = if (dialog != null) TemporaryTextFile(dialog) else null + dialogFile.use { + val processBuilder = ProcessBuilder(createProcessBuilderArgs(dialogFile?.filePath)) + val process: Process = processBuilder.start() + val stdout = BufferedReader(InputStreamReader(process.inputStream, StandardCharsets.UTF_8)) + val stderr = BufferedReader(InputStreamReader(process.errorStream, StandardCharsets.UTF_8)) + try { + while (true) { + val line = stderr.interruptibleReadLine() + + val message = parseJsonObject(line) + when (message.string("type")!!) { + "progress" -> { + progress.reportProgress(message.double("value")!!)} + "success" -> { + progress.reportProgress(1.0) + val resultString = stdout.readText() + return parseRhubarbResult(resultString) + } + "failure" -> { + throw Exception(message.string("reason")) + } + } + } + } catch (e: InterruptedException) { + process.destroyForcibly() + throw e + } catch (e: EOFException) { + throw Exception("Rhubarb terminated unexpectedly.") + } + } + + throw Exception("An unexpected error occurred.") + } + + private fun parseRhubarbResult(jsonString: String): List { + val json = parseJsonObject(jsonString) + val mouthCues = json.array("mouthCues")!! + return mouthCues.map { mouthCue -> + val time = mouthCue.double("start")!! + val mouthShape = MouthShape.valueOf(mouthCue.string("value")!!) + return@map MouthCue(time, mouthShape) + } + } + + private val jsonParser = JsonParser() + private fun parseJsonObject(jsonString: String): JsonObject { + return jsonParser.parse(StringReader(jsonString)) as JsonObject + } + + private fun createProcessBuilderArgs(dialogFilePath: Path?): List { + val extendedMouthShapesString = + if (extendedMouthShapes.any()) extendedMouthShapes.joinToString(separator = "") + else "\"\"" + return mutableListOf( + rhubarbBinFilePath.toString(), + "--machineReadable", + "--exportFormat", "json", + "--extendedShapes", extendedMouthShapesString + ).apply { + if (dialogFilePath != null) { + addAll(listOf( + "--dialogFile", dialogFilePath.toString() + )) + } + }.apply { + add(audioFilePath.toString()) + } + + } + + private val guiBinDirectory: Path by lazy { + var path: String = ClassLoader.getSystemClassLoader().getResource(".")!!.path + if (path.length >= 3 && path[2] == ':') { + // Workaround for https://stackoverflow.com/questions/9834776/java-nio-file-path-issue + path = path.substring(1) + } + return@lazy Paths.get(path) + } + + private val rhubarbBinFilePath: Path by lazy { + val rhubarbBinName = if (IS_OS_WINDOWS) "rhubarb.exe" else "rhubarb" + var currentDirectory: Path? = guiBinDirectory + while (currentDirectory != null) { + val candidate: Path = currentDirectory.resolve(rhubarbBinName) + if (Files.exists(candidate)) { + return@lazy candidate + } + currentDirectory = currentDirectory.parent + } + throw Exception("Could not find Rhubarb Lip Sync executable '$rhubarbBinName'." + + " Expected to find it in '$guiBinDirectory' or any directory above.") + } + + private class TemporaryTextFile(val text: String) : AutoCloseable { + val filePath: Path = Files.createTempFile(null, null).also { + Files.write(it, text.toByteArray(StandardCharsets.UTF_8)) + } + + override fun close() { + Files.delete(filePath) + } + + } + + // Same as readLine, but can be interrupted. + // Note that this function handles linebreak characters differently from readLine. + // It only consumes the first linebreak character before returning and swallows any leading + // linebreak characters. + // This behavior is much easier to implement and doesn't make any difference for our purposes. + private fun BufferedReader.interruptibleReadLine(): String { + val result = StringBuilder() + while (true) { + val char = interruptibleReadChar() + if (char == '\r' || char == '\n') { + if (result.isNotEmpty()) return result.toString() + } else { + result.append(char) + } + } + } + + private fun BufferedReader.interruptibleReadChar(): Char { + while (true) { + if (Thread.currentThread().isInterrupted) { + throw InterruptedException() + } + if (ready()) { + val result: Int = read() + if (result == -1) { + throw EOFException() + } + return result.toChar() + } + Thread.yield() + } + } +} diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/SpineJson.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/SpineJson.kt new file mode 100644 index 0000000..58c3728 --- /dev/null +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/SpineJson.kt @@ -0,0 +1,141 @@ +package com.rhubarb_lip_sync.rhubarb_for_spine + +import com.beust.klaxon.* +import java.nio.file.Files +import java.nio.file.Path + +class SpineJson(val filePath: Path) { + val fileDirectoryPath: Path = filePath.parent + val json: JsonObject + private val skeleton: JsonObject + private val defaultSkin: JsonObject + + init { + if (!Files.exists(filePath)) { + throw Exception("File '$filePath' does not exist.") + } + try { + json = Parser().parse(filePath.toString()) as JsonObject + } catch (e: Exception) { + throw Exception("Wrong file format. This is not a valid JSON file.") + } + skeleton = json.obj("skeleton") ?: throw Exception("JSON file is corrupted.") + val skins = json.obj("skins") ?: throw Exception("JSON file doesn't contain skins.") + defaultSkin = skins.obj("default") ?: throw Exception("JSON file doesn't have a default skin.") + validateProperties() + } + + private fun validateProperties() { + imagesDirectoryPath + audioDirectoryPath + } + + val imagesDirectoryPath: Path get() { + val relativeImagesDirectory = skeleton.string("images") + ?: throw Exception("JSON file is incomplete: Images path is missing." + + "Make sure to check 'Nonessential data' when exporting.") + + val imagesDirectoryPath = fileDirectoryPath.resolve(relativeImagesDirectory) + if (!Files.exists(imagesDirectoryPath)) { + throw Exception("Could not find images directory relative to the JSON file." + + " Make sure the JSON file is in the same directory as the original Spine file.") + } + + return imagesDirectoryPath + } + + val audioDirectoryPath: Path get() { + val relativeAudioDirectory = skeleton.string("audio") + ?: throw Exception("JSON file is incomplete: Audio path is missing." + + "Make sure to check 'Nonessential data' when exporting.") + + val audioDirectoryPath = fileDirectoryPath.resolve(relativeAudioDirectory) + if (!Files.exists(audioDirectoryPath)) { + throw Exception("Could not find audio directory relative to the JSON file." + + " Make sure the JSON file is in the same directory as the original Spine file.") + } + + return audioDirectoryPath + } + + val frameRate: Double get() { + return skeleton.double("fps") ?: 30.0 + } + + val slots: List get() { + val slots = json.array("slots") ?: listOf() + return slots.mapNotNull { it.string("name") } + } + + val presumedMouthSlot: String? get() { + return slots.firstOrNull { it.contains("mouth", ignoreCase = true) } + ?: slots.firstOrNull() + } + + data class AudioEvent(val name: String, val relativeAudioFilePath: String, val dialog: String?) + + val audioEvents: List get() { + val events = json.obj("events") ?: JsonObject() + val result = mutableListOf() + for ((name, value) in events) { + if (value !is JsonObject) throw Exception("Invalid event found.") + + val relativeAudioFilePath = value.string("audio") ?: continue + + val dialog = value.string("string") + result.add(AudioEvent(name, relativeAudioFilePath, dialog)) + } + return result + } + + fun getSlotAttachmentNames(slotName: String): List { + val attachments = defaultSkin.obj(slotName) ?: JsonObject() + return attachments.map { it.key } + } + + fun hasAnimation(animationName: String): Boolean { + val animations = json.obj("animations") ?: return false + return animations.any { it.key == animationName } + } + + fun createOrUpdateAnimation(mouthCues: List, eventName: String, animationName: String, + mouthSlot: String, mouthNaming: MouthNaming + ) { + if (!json.containsKey("animations")) { + json["animations"] = JsonObject() + } + val animations: JsonObject = json.obj("animations")!! + + // Round times to full frames. Always round down. + // If events coincide, prefer the latest one. + val keyframes = mutableMapOf() + for (mouthCue in mouthCues) { + val frameNumber = (mouthCue.time * frameRate).toInt() + keyframes[frameNumber] = mouthCue.mouthShape + } + + animations[animationName] = JsonObject().apply { + this["slots"] = JsonObject().apply { + this[mouthSlot] = JsonObject().apply { + this["attachment"] = JsonArray( + keyframes + .toSortedMap() + .map { (frameNumber, mouthShape) -> + JsonObject().apply { + this["time"] = frameNumber / frameRate + this["name"] = mouthNaming.getName(mouthShape) + } + } + ) + } + } + this["events"] = JsonArray( + JsonObject().apply { + this["time"] = 0.0 + this["name"] = eventName + this["string"] = "" + } + ) + } + } +} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/main.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/main.kt new file mode 100644 index 0000000..1a99446 --- /dev/null +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/main.kt @@ -0,0 +1,7 @@ +package com.rhubarb_lip_sync.rhubarb_for_spine + +import javafx.application.Application + +fun main(args: Array) { + Application.launch(MainApp::class.java, *args) +} diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/tools.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/tools.kt new file mode 100644 index 0000000..7a3cf50 --- /dev/null +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/tools.kt @@ -0,0 +1,23 @@ +package com.rhubarb_lip_sync.rhubarb_for_spine + +import javafx.application.Platform +import javafx.beans.property.Property + +val List.commonPrefix: String get() { + return if (isEmpty()) "" else this.reduce { result, string -> result.commonPrefixWith(string) } +} + +val List.commonSuffix: String get() { + return if (isEmpty()) "" else this.reduce { result, string -> result.commonSuffixWith(string) } +} + +fun > TProperty.applyListener(listener: (TValue) -> Unit) : TProperty { + // Notify the listener of the initial value. + // If we did this synchronously, the listener's state would have to be fully initialized the + // moment this function is called. So calling this function during object initialization might + // result in access to uninitialized state. + Platform.runLater { listener(this.value) } + + addListener({ _, _, newValue -> listener(newValue)}) + return this +} From 7bb1bec975848b3eb60844094f09f9d301e7bcc3 Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Mon, 8 Jan 2018 15:05:31 +0100 Subject: [PATCH 08/33] Implemented round trip Spine - Rhubarb - Spine --- .../rhubarb_for_spine/AnimationFileModel.kt | 85 +++++++++- .../rhubarb_for_spine/AudioFileModel.kt | 158 ++++++++++++++++++ .../rhubarb_for_spine/MainModel.kt | 9 +- .../rhubarb_for_spine/MainView.kt | 105 +++++++++--- .../rhubarb_for_spine/MouthNaming.kt | 13 +- .../rhubarb_for_spine/MouthShape.kt | 16 +- .../rhubarb_for_spine/Progress.kt | 6 - .../rhubarb_for_spine/RhubarbTask.kt | 28 ++-- .../rhubarb_for_spine/SpineJson.kt | 8 +- .../rhubarb_for_spine/tools.kt | 50 +++++- 10 files changed, 420 insertions(+), 58 deletions(-) create mode 100644 extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AudioFileModel.kt delete mode 100644 extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/Progress.kt diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt index 1c1a1d0..14a6df3 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt @@ -1,11 +1,92 @@ package com.rhubarb_lip_sync.rhubarb_for_spine +import javafx.beans.property.SimpleListProperty +import javafx.beans.property.SimpleObjectProperty +import javafx.beans.property.SimpleStringProperty +import javafx.collections.ObservableList import java.nio.file.Path +import tornadofx.getValue +import tornadofx.observable +import tornadofx.setValue +import java.util.concurrent.Executors class AnimationFileModel(animationFilePath: Path) { - val spineJson: SpineJson + val spineJson = SpineJson(animationFilePath) + + val slotsProperty = SimpleObjectProperty>() + var slots by slotsProperty + private set + + val mouthSlotProperty: SimpleStringProperty = SimpleStringProperty().alsoListen { + mouthNaming = if (mouthSlot != null) + MouthNaming.guess(spineJson.getSlotAttachmentNames(mouthSlot)) + else null + + mouthShapes = if (mouthSlot != null) { + val mouthNames = spineJson.getSlotAttachmentNames(mouthSlot) + MouthShape.values().filter { mouthNames.contains(mouthNaming.getName(it)) } + } else null + + mouthSlotError = if (mouthSlot != null) null + else "No slot with mouth drawings specified." + } + var mouthSlot by mouthSlotProperty + + val mouthSlotErrorProperty = SimpleStringProperty() + var mouthSlotError by mouthSlotErrorProperty + private set + + val mouthNamingProperty = SimpleObjectProperty() + var mouthNaming by mouthNamingProperty + private set + + val mouthShapesProperty = SimpleObjectProperty>().alsoListen { + mouthShapesError = getMouthShapesErrorString() + } + var mouthShapes by mouthShapesProperty + private set + + val mouthShapesErrorProperty = SimpleStringProperty() + var mouthShapesError by mouthShapesErrorProperty + private set + + val audioFileModelsProperty = SimpleListProperty( + spineJson.audioEvents + .map { event -> + val executor = Executors.newSingleThreadExecutor() + AudioFileModel(event, this, executor, { result -> saveAnimation(result, event.name) }) + } + .observable() + ) + + private fun saveAnimation(mouthCues: List, audioEventName: String) { + val animationName = getAnimationName(audioEventName) + spineJson.createOrUpdateAnimation(mouthCues, audioEventName, animationName, mouthSlot, mouthNaming) + spineJson.save() + } + + private fun getAnimationName(audioEventName: String): String = "say_$audioEventName" + + val audioFileModels by audioFileModelsProperty init { - spineJson = SpineJson(animationFilePath) + slots = spineJson.slots.observable() + mouthSlot = spineJson.guessMouthSlot() } + + private fun getMouthShapesErrorString(): String? { + val missingBasicShapes = MouthShape.basicShapes + .filter{ !mouthShapes.contains(it) } + if (missingBasicShapes.isEmpty()) return null + + val result = StringBuilder() + result.append("Mouth shapes ${missingBasicShapes.joinToString()}") + result.appendln(if (missingBasicShapes.count() > 1) " are missing." else " is missing.") + + val first = MouthShape.basicShapes.first() + val last = MouthShape.basicShapes.last() + result.append("At least the basic mouth shapes $first-$last need corresponding image attachments.") + return result.toString() + } + } \ No newline at end of file diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AudioFileModel.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AudioFileModel.kt new file mode 100644 index 0000000..fb4b66e --- /dev/null +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AudioFileModel.kt @@ -0,0 +1,158 @@ +package com.rhubarb_lip_sync.rhubarb_for_spine + +import javafx.application.Platform +import javafx.beans.binding.ObjectBinding +import javafx.beans.binding.StringBinding +import javafx.beans.property.SimpleBooleanProperty +import javafx.beans.property.SimpleObjectProperty +import javafx.beans.property.SimpleStringProperty +import javafx.scene.control.Alert +import tornadofx.getValue +import tornadofx.setValue +import java.util.concurrent.ExecutorService +import java.util.concurrent.Future + +class AudioFileModel( + audioEvent: SpineJson.AudioEvent, + private val parentModel: AnimationFileModel, + private val executor: ExecutorService, + private val reportResult: (List) -> Unit +) { + val spineJson = parentModel.spineJson + + val audioFilePath = spineJson.audioDirectoryPath.resolve(audioEvent.relativeAudioFilePath) + + val eventNameProperty = SimpleStringProperty(audioEvent.name) + val eventName by eventNameProperty + + val displayFilePathProperty = SimpleStringProperty(audioEvent.relativeAudioFilePath) + val displayFilePath by displayFilePathProperty + + val dialogProperty = SimpleStringProperty(audioEvent.dialog) + val dialog: String? by dialogProperty + + val animationProgressProperty = SimpleObjectProperty(null) + var animationProgress by animationProgressProperty + private set + + private val animatedPreviouslyProperty = SimpleBooleanProperty(false) // TODO: Initial value + private var animatedPreviously by animatedPreviouslyProperty + + private val futureProperty = SimpleObjectProperty?>() + private var future by futureProperty + + private val audioFileStateProperty = SimpleObjectProperty().apply { + bind(object : ObjectBinding() { + init { + super.bind(animatedPreviouslyProperty, futureProperty, animationProgressProperty) + } + override fun computeValue(): AudioFileState { + return if (future != null) { + if (animationProgress != null) + if (future!!.isCancelled) + AudioFileState(AudioFileStatus.Canceling) + else + AudioFileState(AudioFileStatus.Animating, animationProgress) + else + AudioFileState(AudioFileStatus.Pending) + } else { + if (animatedPreviously) + AudioFileState(AudioFileStatus.Done) + else + AudioFileState(AudioFileStatus.NotAnimated) + } + } + }) + } + private val audioFileState by audioFileStateProperty + + val statusLabelProperty = SimpleStringProperty().apply { + bind(object : StringBinding() { + init { + super.bind(audioFileStateProperty) + } + override fun computeValue(): String { + return when (audioFileState.status) { + AudioFileStatus.NotAnimated -> "" + AudioFileStatus.Pending -> "Waiting" + AudioFileStatus.Animating -> "${((animationProgress ?: 0.0) * 100).toInt()}%" + AudioFileStatus.Canceling -> "Canceling" + AudioFileStatus.Done -> "Done" + } + } + }) + } + val statusLabel by statusLabelProperty + + val actionLabelProperty = SimpleStringProperty().apply { + bind(object : StringBinding() { + init { + super.bind(futureProperty) + } + override fun computeValue(): String { + return if (future != null) + "Cancel" + else + "Animate" + } + }) + } + val actionLabel by actionLabelProperty + + fun performAction() { + if (future == null) { + startAnimation() + } else { + cancelAnimation() + } + } + + private fun startAnimation() { + val wrapperTask = Runnable { + val extendedMouthShapes = parentModel.mouthShapes.filter { it.isExtended }.toSet() + val reportProgress: (Double?) -> Unit = { + progress -> runAndWait { this@AudioFileModel.animationProgress = progress } + } + val rhubarbTask = RhubarbTask(audioFilePath, dialog, extendedMouthShapes, reportProgress) + try { + try { + val result = rhubarbTask.call() + runAndWait { + reportResult(result) + animatedPreviously = true + } + } finally { + runAndWait { + animationProgress = null + future = null + } + } + } catch (e: InterruptedException) { + } catch (e: Exception) { + Platform.runLater { + Alert(Alert.AlertType.ERROR).apply { + headerText = "Error performing lip-sync for event $eventName." + contentText = e.toString() + show() + } + } + } + + } + future = executor.submit(wrapperTask) + } + + private fun cancelAnimation() { + future?.cancel(true) + } +} + +enum class AudioFileStatus { + NotAnimated, + Pending, + Animating, + Canceling, + Done +} + +data class AudioFileState(val status: AudioFileStatus, val progress: Double? = null) \ No newline at end of file diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainModel.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainModel.kt index ca3dcb2..678e436 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainModel.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainModel.kt @@ -10,8 +10,8 @@ import java.nio.file.InvalidPathException import java.nio.file.Paths class MainModel { - val filePathStringProperty = SimpleStringProperty(getDefaultPathString()).applyListener { value -> - try { + val filePathStringProperty = SimpleStringProperty(getDefaultPathString()).alsoListen { value -> + filePathError = getExceptionMessage { animationFileModel = null if (value.isNullOrBlank()) { throw Exception("No input file specified.") @@ -29,13 +29,8 @@ class MainModel { } animationFileModel = AnimationFileModel(path) - - filePathError = null - } catch (e: Exception) { - filePathError = e.message } } - var filePathString by filePathStringProperty val filePathErrorProperty = SimpleStringProperty() diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt index 85d5b72..38ec0bf 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt @@ -1,10 +1,17 @@ package com.rhubarb_lip_sync.rhubarb_for_spine +import javafx.beans.property.SimpleStringProperty +import javafx.event.ActionEvent import javafx.event.EventHandler +import javafx.scene.control.Button +import javafx.scene.control.TableCell +import javafx.scene.control.TableView import javafx.scene.control.TextField import javafx.scene.input.DragEvent import javafx.scene.input.TransferMode +import javafx.stage.FileChooser import tornadofx.* +import java.io.File import java.time.LocalDate import java.time.Period @@ -16,47 +23,85 @@ class MainView : View() { val age: Int get() = Period.between(birthday, LocalDate.now()).years } - private val persons = listOf( - Person(1,"Samantha Stuart",LocalDate.of(1981,12,4)), - Person(2,"Tom Marks",LocalDate.of(2001,1,23)), - Person(3,"Stuart Gills",LocalDate.of(1989,5,23)), - Person(3,"Nicole Williams",LocalDate.of(1998,8,11)) - ).observable() - init { title = "Rhubarb Lip Sync for Spine" } override val root = form { - var filePathField: TextField? = null + var filePathTextField: TextField? = null + var filePathButton: Button? = null + + val fileModelProperty = mainModel.animationFileModelProperty minWidth = 800.0 + prefWidth = 1000.0 fieldset("Settings") { field("Spine JSON file") { - filePathField = textfield { + filePathTextField = textfield { textProperty().bindBidirectional(mainModel.filePathStringProperty) - tooltip("Hello world") errorProperty().bind(mainModel.filePathErrorProperty) } - button("...") + filePathButton = button("...") } field("Mouth slot") { - textfield() + combobox { + itemsProperty().bind(fileModelProperty.select { it!!.slotsProperty }) + valueProperty().bindBidirectional(fileModelProperty.select { it!!.mouthSlotProperty }) + errorProperty().bind(fileModelProperty.select { it!!.mouthSlotErrorProperty }) + } } field("Mouth naming") { - datepicker() + label { + textProperty().bind( + fileModelProperty + .select { it!!.mouthNamingProperty } + .select { SimpleStringProperty(it.displayString) } + ) + } } field("Mouth shapes") { - textfield() + hbox { + label { + textProperty().bind( + fileModelProperty + .select { it!!.mouthShapesProperty } + .select { + val result = if (it.isEmpty()) "none" else it.joinToString() + SimpleStringProperty(result) + } + ) + } + errorProperty().bind(fileModelProperty.select { it!!.mouthShapesErrorProperty }) + } } } fieldset("Audio events") { - tableview(persons) { - column("Event", Person::id) - column("Audio file", Person::name) - column("Dialog", Person::birthday) - column("Status", Person::age) - column("", Person::age) + tableview { + columnResizePolicy = TableView.CONSTRAINED_RESIZE_POLICY + column("Event", AudioFileModel::eventNameProperty) + column("Audio file", AudioFileModel::displayFilePathProperty) + column("Dialog", AudioFileModel::dialogProperty) + column("Status", AudioFileModel::statusLabelProperty) + column("", AudioFileModel::actionLabelProperty).apply { + setCellFactory { tableColumn -> + return@setCellFactory object : TableCell() { + override fun updateItem(item: String?, empty: Boolean) { + super.updateItem(item, empty) + graphic = if (!empty) + Button(item).apply { + this.maxWidth = Double.MAX_VALUE + setOnAction { + val audioFileModel = this@tableview.items[index] + audioFileModel.performAction() + } + } + else + null + } + } + } + } + itemsProperty().bind(fileModelProperty.select { it!!.audioFileModelsProperty }) } } @@ -68,10 +113,28 @@ class MainView : View() { } onDragDropped = EventHandler { event -> if (event.dragboard.hasFiles()) { - filePathField!!.text = event.dragboard.files.firstOrNull()?.path + filePathTextField!!.text = event.dragboard.files.firstOrNull()?.path event.isDropCompleted = true event.consume() } } + + filePathButton!!.onAction = EventHandler { + val fileChooser = FileChooser().apply { + title = "Open Spine JSON file" + extensionFilters.addAll( + FileChooser.ExtensionFilter("Spine JSON file (*.json)", "*.json"), + FileChooser.ExtensionFilter("All files (*.*)", "*.*") + ) + val lastDirectory = filePathTextField!!.text?.let { File(it).parentFile } + if (lastDirectory != null && lastDirectory.isDirectory) { + initialDirectory = lastDirectory + } + } + val file = fileChooser.showOpenDialog(this@MainView.primaryStage) + if (file != null) { + filePathTextField!!.text = file.path + } + } } } \ No newline at end of file diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MouthNaming.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MouthNaming.kt index 25dbc21..8b6416b 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MouthNaming.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MouthNaming.kt @@ -6,13 +6,20 @@ class MouthNaming(val prefix: String, val suffix: String, val mouthShapeCasing: companion object { fun guess(mouthNames: List): MouthNaming { - if (mouthNames.isEmpty()) return MouthNaming("", "", guessMouthShapeCasing("")) + if (mouthNames.isEmpty()) { + return MouthNaming("", "", guessMouthShapeCasing("")) + } val commonPrefix = mouthNames.commonPrefix val commonSuffix = mouthNames.commonSuffix - val shapeName = mouthNames.first().substring( + val firstMouthName = mouthNames.first() + if (commonPrefix.length + commonSuffix.length >= firstMouthName.length) { + return MouthNaming(commonPrefix, "", guessMouthShapeCasing("")) + } + + val shapeName = firstMouthName.substring( commonPrefix.length, - mouthNames.first().length - commonSuffix.length) + firstMouthName.length - commonSuffix.length) val mouthShapeCasing = guessMouthShapeCasing(shapeName) return MouthNaming(commonPrefix, commonSuffix, mouthShapeCasing) } diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MouthShape.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MouthShape.kt index 5f27ec0..c41a425 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MouthShape.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MouthShape.kt @@ -1,5 +1,17 @@ package com.rhubarb_lip_sync.rhubarb_for_spine enum class MouthShape { - A, B, C, D, E, F, G, H, X -} \ No newline at end of file + A, B, C, D, E, F, G, H, X; + + val isBasic: Boolean + get() = this.ordinal < basicShapeCount + + val isExtended: Boolean + get() = !this.isBasic + + companion object { + val basicShapeCount = 6 + + val basicShapes = MouthShape.values().take(basicShapeCount) + } +} diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/Progress.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/Progress.kt deleted file mode 100644 index d8ce1bf..0000000 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/Progress.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.rhubarb_lip_sync.rhubarb_for_spine - -// Modeled after C#'s IProgress -interface Progress { - fun reportProgress(progress: Double) -} \ No newline at end of file diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/RhubarbTask.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/RhubarbTask.kt index 24ede9f..34a5505 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/RhubarbTask.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/RhubarbTask.kt @@ -6,10 +6,7 @@ import com.beust.klaxon.double import com.beust.klaxon.string import com.beust.klaxon.Parser as JsonParser import org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS -import java.io.BufferedReader -import java.io.EOFException -import java.io.InputStreamReader -import java.io.StringReader +import java.io.* import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.Path @@ -21,7 +18,7 @@ class RhubarbTask( val audioFilePath: Path, val dialog: String?, val extendedMouthShapes: Set, - val progress: Progress + val reportProgress: (Double?) -> Unit ) : Callable> { override fun call(): List { @@ -32,24 +29,25 @@ class RhubarbTask( throw IllegalArgumentException("File '$audioFilePath' does not exist."); } - val dialogFile = if (dialog != null) TemporaryTextFile(dialog) else null - dialogFile.use { - val processBuilder = ProcessBuilder(createProcessBuilderArgs(dialogFile?.filePath)) + val outputFile = TemporaryTextFile() + dialogFile.use { outputFile.use { + val processBuilder = ProcessBuilder(createProcessBuilderArgs(dialogFile?.filePath)).apply { + // See http://java-monitor.com/forum/showthread.php?t=4067 + redirectOutput(outputFile.filePath.toFile()) + } val process: Process = processBuilder.start() - val stdout = BufferedReader(InputStreamReader(process.inputStream, StandardCharsets.UTF_8)) val stderr = BufferedReader(InputStreamReader(process.errorStream, StandardCharsets.UTF_8)) try { while (true) { val line = stderr.interruptibleReadLine() - val message = parseJsonObject(line) when (message.string("type")!!) { "progress" -> { - progress.reportProgress(message.double("value")!!)} + reportProgress(message.double("value")!!)} "success" -> { - progress.reportProgress(1.0) - val resultString = stdout.readText() + reportProgress(1.0) + val resultString = String(Files.readAllBytes(outputFile.filePath), StandardCharsets.UTF_8) return parseRhubarbResult(resultString) } "failure" -> { @@ -63,7 +61,7 @@ class RhubarbTask( } catch (e: EOFException) { throw Exception("Rhubarb terminated unexpectedly.") } - } + }} throw Exception("An unexpected error occurred.") } @@ -127,7 +125,7 @@ class RhubarbTask( + " Expected to find it in '$guiBinDirectory' or any directory above.") } - private class TemporaryTextFile(val text: String) : AutoCloseable { + private class TemporaryTextFile(text: String = "") : AutoCloseable { val filePath: Path = Files.createTempFile(null, null).also { Files.write(it, text.toByteArray(StandardCharsets.UTF_8)) } diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/SpineJson.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/SpineJson.kt index 58c3728..91edb5f 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/SpineJson.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/SpineJson.kt @@ -1,6 +1,7 @@ package com.rhubarb_lip_sync.rhubarb_for_spine import com.beust.klaxon.* +import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.Path @@ -67,7 +68,7 @@ class SpineJson(val filePath: Path) { return slots.mapNotNull { it.string("name") } } - val presumedMouthSlot: String? get() { + fun guessMouthSlot(): String? { return slots.firstOrNull { it.contains("mouth", ignoreCase = true) } ?: slots.firstOrNull() } @@ -138,4 +139,9 @@ class SpineJson(val filePath: Path) { ) } } + + fun save() { + var string = json.toJsonString(prettyPrint = true) + Files.write(filePath, listOf(string), StandardCharsets.UTF_8) + } } \ No newline at end of file diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/tools.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/tools.kt index 7a3cf50..c5811c0 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/tools.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/tools.kt @@ -2,6 +2,11 @@ package com.rhubarb_lip_sync.rhubarb_for_spine import javafx.application.Platform import javafx.beans.property.Property +import java.util.concurrent.ExecutionException +import java.util.concurrent.locks.Condition +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + val List.commonPrefix: String get() { return if (isEmpty()) "" else this.reduce { result, string -> result.commonPrefixWith(string) } @@ -11,7 +16,7 @@ val List.commonSuffix: String get() { return if (isEmpty()) "" else this.reduce { result, string -> result.commonSuffixWith(string) } } -fun > TProperty.applyListener(listener: (TValue) -> Unit) : TProperty { +fun > TProperty.alsoListen(listener: (TValue) -> Unit) : TProperty { // Notify the listener of the initial value. // If we did this synchronously, the listener's state would have to be fully initialized the // moment this function is called. So calling this function during object initialization might @@ -21,3 +26,46 @@ fun > TProperty.applyListener(listener: (TV addListener({ _, _, newValue -> listener(newValue)}) return this } + +fun getExceptionMessage(action: () -> Unit): String? { + try { + action(); + } catch (e: Exception) { + return e.message + } + return null +} + + +/** + * Invokes a Runnable on the JFX thread and waits until it's finished. + * Similar to SwingUtilities.invokeAndWait. + * Based on http://www.guigarage.com/2013/01/invokeandwait-for-javafx/ + * + * @throws InterruptedException Execution was interrupted + * @throws Throwable An exception occurred in the run method of the Runnable + */ +fun runAndWait(action: () -> Unit) { + if (Platform.isFxApplicationThread()) { + action() + } else { + val lock = ReentrantLock() + lock.withLock { + val doneCondition = lock.newCondition() + var throwable: Throwable? = null + Platform.runLater { + lock.withLock { + try { + action() + } catch (e: Throwable) { + throwable = e + } finally { + doneCondition.signal() + } + } + } + doneCondition.await() + throwable?.let { throw it } + } + } +} \ No newline at end of file From db3615b3c3aa62f681b6998ef4a578842d4f92aa Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Wed, 24 Jan 2018 19:21:46 +0100 Subject: [PATCH 09/33] Updated Gradle wrapper files --- .../gradle/wrapper/gradle-wrapper.jar | Bin 54788 -> 54727 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 ++--- extras/rhubarb-for-spine/gradlew | 6 +++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/extras/rhubarb-for-spine/gradle/wrapper/gradle-wrapper.jar b/extras/rhubarb-for-spine/gradle/wrapper/gradle-wrapper.jar index 5c37b441829ca7790c5885480c6c4c424602c39f..27768f1bbac3ce2d055b20d521f12da78d331e8e 100644 GIT binary patch delta 18517 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 z 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 R9}M+waEGeMk>0(p`afE}t#1GT diff --git a/extras/rhubarb-for-spine/gradle/wrapper/gradle-wrapper.properties b/extras/rhubarb-for-spine/gradle/wrapper/gradle-wrapper.properties index 22c733c..62e1e30 100644 --- a/extras/rhubarb-for-spine/gradle/wrapper/gradle-wrapper.properties +++ b/extras/rhubarb-for-spine/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Tue Nov 28 18:16:46 CET 2017 +distributionUrl=https\://services.gradle.org/distributions/gradle-4.3-bin.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-rc-2-all.zip +zipStoreBase=GRADLE_USER_HOME diff --git a/extras/rhubarb-for-spine/gradlew b/extras/rhubarb-for-spine/gradlew index 4453cce..cccdd3d 100644 --- a/extras/rhubarb-for-spine/gradlew +++ b/extras/rhubarb-for-spine/gradlew @@ -33,11 +33,11 @@ DEFAULT_JVM_OPTS="" # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -155,7 +155,7 @@ if $cygwin ; then fi # Escape application args -save ( ) { +save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } From 61731441b3a7bd712e98a2f7f893ad37e28d11a2 Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Wed, 24 Jan 2018 20:10:11 +0100 Subject: [PATCH 10/33] Made JAR file executable --- extras/rhubarb-for-spine/build.gradle | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/extras/rhubarb-for-spine/build.gradle b/extras/rhubarb-for-spine/build.gradle index d39c4cf..21e6aa7 100644 --- a/extras/rhubarb-for-spine/build.gradle +++ b/extras/rhubarb-for-spine/build.gradle @@ -42,4 +42,15 @@ compileKotlin { } compileTestKotlin { kotlinOptions.jvmTarget = '1.8' +} + +jar { + manifest { + attributes 'Main-Class': 'com.rhubarb_lip_sync.rhubarb_for_spine.MainKt' + } + + // This line of code recursively collects and copies all of a project's files + // and adds them to the JAR itself. One can extend this task, to skip certain + // files or particular types at will + from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } } \ No newline at end of file From 19a31445729fb3d9214fc1a422481675590da689 Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Fri, 2 Feb 2018 21:28:57 +0100 Subject: [PATCH 11/33] Integrated Rhubarb for Spine into build --- CMakeLists.txt | 9 ++++----- extras/AdobeAfterEffects/CMakeLists.txt | 11 +++++++++++ extras/MagixVegas/CMakeLists.txt | 14 ++++++++++++++ extras/rhubarb-for-spine/.gitignore | 3 ++- extras/rhubarb-for-spine/CMakeLists.txt | 18 ++++++++++++++++++ extras/rhubarb-for-spine/README.md | 3 +++ extras/rhubarb-for-spine/gradlew | 0 7 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 extras/AdobeAfterEffects/CMakeLists.txt create mode 100644 extras/MagixVegas/CMakeLists.txt create mode 100644 extras/rhubarb-for-spine/CMakeLists.txt create mode 100644 extras/rhubarb-for-spine/README.md mode change 100644 => 100755 extras/rhubarb-for-spine/gradlew diff --git a/CMakeLists.txt b/CMakeLists.txt index 81b1cb0..14c8eab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,11 +5,10 @@ include(appInfo.cmake) # Build and install main executable add_subdirectory(rhubarb) -# Install extras -install( - DIRECTORY extras - DESTINATION . -) +# Build and install extras +add_subdirectory("extras/AdobeAfterEffects") +add_subdirectory("extras/MagixVegas") +add_subdirectory("extras/rhubarb-for-spine") # Install misc. files install( diff --git a/extras/AdobeAfterEffects/CMakeLists.txt b/extras/AdobeAfterEffects/CMakeLists.txt new file mode 100644 index 0000000..c69164b --- /dev/null +++ b/extras/AdobeAfterEffects/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.2) + +set(afterEffectsFiles + "Rhubarb Lip Sync.jsx" + "README.md" +) + +install( + FILES ${afterEffectsFiles} + DESTINATION "extras/AdobeAfterEffects" +) diff --git a/extras/MagixVegas/CMakeLists.txt b/extras/MagixVegas/CMakeLists.txt new file mode 100644 index 0000000..ba9d45b --- /dev/null +++ b/extras/MagixVegas/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.2) + +set(vegasFiles + "Debug Rhubarb.cs" + "Debug Rhubarb.cs.config" + "Import Rhubarb.cs" + "Import Rhubarb.cs.config" + "README.md" +) + +install( + FILES ${vegasFiles} + DESTINATION "extras/MagixVegas" +) diff --git a/extras/rhubarb-for-spine/.gitignore b/extras/rhubarb-for-spine/.gitignore index 5889e76..2753810 100644 --- a/extras/rhubarb-for-spine/.gitignore +++ b/extras/rhubarb-for-spine/.gitignore @@ -10,4 +10,5 @@ /.idea/**/*.iml /build/ -/out/ \ No newline at end of file +/out/ +/tmp/ diff --git a/extras/rhubarb-for-spine/CMakeLists.txt b/extras/rhubarb-for-spine/CMakeLists.txt new file mode 100644 index 0000000..a4fb7cb --- /dev/null +++ b/extras/rhubarb-for-spine/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.2) + +add_custom_target( + rhubarbForSpine ALL + "./gradlew" "jar" + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + COMMENT "Building Rhubarb for Spine through Gradle." +) + +install( + DIRECTORY "build/libs/" + DESTINATION "extras/rhubarb-for-spine" +) + +install( + FILES README.md + DESTINATION "extras/rhubarb-for-spine" +) diff --git a/extras/rhubarb-for-spine/README.md b/extras/rhubarb-for-spine/README.md new file mode 100644 index 0000000..dcabdcf --- /dev/null +++ b/extras/rhubarb-for-spine/README.md @@ -0,0 +1,3 @@ +# Rhubarb Lip Sync for Spine + +TODO: Document how to use \ No newline at end of file diff --git a/extras/rhubarb-for-spine/gradlew b/extras/rhubarb-for-spine/gradlew old mode 100644 new mode 100755 From 34610d05723be029e296d646fb5c6df5b37cd148 Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Fri, 2 Feb 2018 21:49:07 +0100 Subject: [PATCH 12/33] Preventing more than one audio file from being processed simultaneously --- .../rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt index 14a6df3..3a1f610 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt @@ -50,10 +50,10 @@ class AnimationFileModel(animationFilePath: Path) { var mouthShapesError by mouthShapesErrorProperty private set + private val executor = Executors.newSingleThreadExecutor() val audioFileModelsProperty = SimpleListProperty( spineJson.audioEvents .map { event -> - val executor = Executors.newSingleThreadExecutor() AudioFileModel(event, this, executor, { result -> saveAnimation(result, event.name) }) } .observable() From 6e5af62be75cd7f184f17f8241038599b6e0d820 Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Fri, 2 Feb 2018 22:02:34 +0100 Subject: [PATCH 13/33] Shutting down executor service to that application can exit cleanly --- .../rhubarb_for_spine/AnimationFileModel.kt | 5 ++--- .../com/rhubarb_lip_sync/rhubarb_for_spine/MainModel.kt | 5 +++-- .../com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt | 9 +++++++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt index 3a1f610..005c784 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt @@ -8,9 +8,9 @@ import java.nio.file.Path import tornadofx.getValue import tornadofx.observable import tornadofx.setValue -import java.util.concurrent.Executors +import java.util.concurrent.ExecutorService -class AnimationFileModel(animationFilePath: Path) { +class AnimationFileModel(animationFilePath: Path, private val executor: ExecutorService) { val spineJson = SpineJson(animationFilePath) val slotsProperty = SimpleObjectProperty>() @@ -50,7 +50,6 @@ class AnimationFileModel(animationFilePath: Path) { var mouthShapesError by mouthShapesErrorProperty private set - private val executor = Executors.newSingleThreadExecutor() val audioFileModelsProperty = SimpleListProperty( spineJson.audioEvents .map { event -> diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainModel.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainModel.kt index 678e436..f860a44 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainModel.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainModel.kt @@ -8,8 +8,9 @@ import tornadofx.setValue import java.nio.file.Files import java.nio.file.InvalidPathException import java.nio.file.Paths +import java.util.concurrent.ExecutorService -class MainModel { +class MainModel(private val executor: ExecutorService) { val filePathStringProperty = SimpleStringProperty(getDefaultPathString()).alsoListen { value -> filePathError = getExceptionMessage { animationFileModel = null @@ -28,7 +29,7 @@ class MainModel { throw Exception("File does not exist.") } - animationFileModel = AnimationFileModel(path) + animationFileModel = AnimationFileModel(path, executor) } } var filePathString by filePathStringProperty diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt index 38ec0bf..7b24be0 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt @@ -14,10 +14,11 @@ import tornadofx.* import java.io.File import java.time.LocalDate import java.time.Period +import java.util.concurrent.Executors class MainView : View() { - - val mainModel = MainModel() + private val executor = Executors.newSingleThreadExecutor() + private val mainModel = MainModel(executor) class Person(val id: Int, val name: String, val birthday: LocalDate) { val age: Int get() = Period.between(birthday, LocalDate.now()).years @@ -119,6 +120,10 @@ class MainView : View() { } } + whenUndocked { + executor.shutdownNow() + } + filePathButton!!.onAction = EventHandler { val fileChooser = FileChooser().apply { title = "Open Spine JSON file" From d9d0f37b5a419f1a8b4b2b35a3d42f83d7bddca6 Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Fri, 2 Feb 2018 22:23:49 +0100 Subject: [PATCH 14/33] Dialog column wraps its text --- .../rhubarb_for_spine/MainView.kt | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt index 7b24be0..e15a50a 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt @@ -3,12 +3,11 @@ package com.rhubarb_lip_sync.rhubarb_for_spine import javafx.beans.property.SimpleStringProperty import javafx.event.ActionEvent import javafx.event.EventHandler -import javafx.scene.control.Button -import javafx.scene.control.TableCell -import javafx.scene.control.TableView -import javafx.scene.control.TextField +import javafx.scene.control.* import javafx.scene.input.DragEvent import javafx.scene.input.TransferMode +import javafx.scene.paint.Paint +import javafx.scene.text.Text import javafx.stage.FileChooser import tornadofx.* import java.io.File @@ -81,9 +80,22 @@ class MainView : View() { columnResizePolicy = TableView.CONSTRAINED_RESIZE_POLICY column("Event", AudioFileModel::eventNameProperty) column("Audio file", AudioFileModel::displayFilePathProperty) - column("Dialog", AudioFileModel::dialogProperty) + column("Dialog", AudioFileModel::dialogProperty).apply { + // Make dialog column wrap + setCellFactory { tableColumn -> + return@setCellFactory TableCell().also { cell -> + cell.graphic = Text().apply { + textProperty().bind(cell.itemProperty()) + fillProperty().bind(cell.textFillProperty()) + wrappingWidthProperty().bind(tableColumn.widthProperty()) + } + cell.prefHeight = Control.USE_COMPUTED_SIZE + } + } + } column("Status", AudioFileModel::statusLabelProperty) column("", AudioFileModel::actionLabelProperty).apply { + // Show button setCellFactory { tableColumn -> return@setCellFactory object : TableCell() { override fun updateItem(item: String?, empty: Boolean) { From ecc8efc56509af874da1c80ecedd68ce89a086c5 Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Fri, 2 Feb 2018 23:05:17 +0100 Subject: [PATCH 15/33] Added window icon --- .../rhubarb_for_spine/MainView.kt | 13 ++++++------- .../src/main/resources/icon-16.png | Bin 0 -> 386 bytes .../src/main/resources/icon-256.png | Bin 0 -> 7080 bytes .../src/main/resources/icon-32.png | Bin 0 -> 942 bytes .../src/main/resources/icon-48.png | Bin 0 -> 1398 bytes 5 files changed, 6 insertions(+), 7 deletions(-) create mode 100644 extras/rhubarb-for-spine/src/main/resources/icon-16.png create mode 100644 extras/rhubarb-for-spine/src/main/resources/icon-256.png create mode 100644 extras/rhubarb-for-spine/src/main/resources/icon-32.png create mode 100644 extras/rhubarb-for-spine/src/main/resources/icon-48.png diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt index e15a50a..98f24fa 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt @@ -4,27 +4,26 @@ import javafx.beans.property.SimpleStringProperty import javafx.event.ActionEvent import javafx.event.EventHandler import javafx.scene.control.* +import javafx.scene.image.Image import javafx.scene.input.DragEvent import javafx.scene.input.TransferMode -import javafx.scene.paint.Paint import javafx.scene.text.Text import javafx.stage.FileChooser import tornadofx.* import java.io.File -import java.time.LocalDate -import java.time.Period import java.util.concurrent.Executors class MainView : View() { private val executor = Executors.newSingleThreadExecutor() private val mainModel = MainModel(executor) - class Person(val id: Int, val name: String, val birthday: LocalDate) { - val age: Int get() = Period.between(birthday, LocalDate.now()).years - } - init { title = "Rhubarb Lip Sync for Spine" + + // Set icon + for (iconSize in listOf(16, 32, 48, 256)) { + addStageIcon(Image(this.javaClass.getResourceAsStream("/icon-$iconSize.png"))) + } } override val root = form { diff --git a/extras/rhubarb-for-spine/src/main/resources/icon-16.png b/extras/rhubarb-for-spine/src/main/resources/icon-16.png new file mode 100644 index 0000000000000000000000000000000000000000..5ac439bd0c6b87805f08516b01509a704626f715 GIT binary patch literal 386 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbK}T7XZ8>;M1%y}UeEMYjG=oC;(B z(UK*Lfr1fW;O*^&j6y(kv$F~c@=HrgGBY#s@^XP}pqj|Y$k^DJ$jFG`;2;Pi zCMJ4Wa0AfTWkL0Q48K55VJ!*r1KG^5c>?p3b|B4J;1OBOz#uLN!i=ZXKHCEY*-Jcq zUD+RUv5A=SoZG+PG*GD0)5S4FLX!31!9W&60S3dZxvPFmIPg^dwjkr44}3v$p7G6j zcB*}6!`%I~Ud&}LRX8_SE!#UQH8r&V_AZ+S_U{LHm=Di9xo%b6ddqhoBSZf_GN=u^ z`dz^)?!&iqotfclt9WnD`uVe~GTy_YfPHi7IoZ!^8W+eG^ZsMZ?6q)Ts>8k==m^OY v*NBqf{Irtt#G+IN$CUh}R0Yr6#Prml)Wnp^!jq{sKt&9mu6{1-oD!M<=l71I literal 0 HcmV?d00001 diff --git a/extras/rhubarb-for-spine/src/main/resources/icon-256.png b/extras/rhubarb-for-spine/src/main/resources/icon-256.png new file mode 100644 index 0000000000000000000000000000000000000000..bbeaebd3509e8d13ee06708ee2ed8b07ea0a83f3 GIT binary patch literal 7080 zcmbt(_gB-;^Ytqs2_dx5qzIuY(u;^l550E~6{HF%J#?gn-V{U>cuNZ?qM(3M6+%Ks zL5ftV5vBK{1PJ8u^ZX0nIcI0~?43Jj&;GEx=T4f1nLZOCgb(2buu_nGkG)qT#*Odt)@_} zR%1=u36?)xp^w|+{>JPS)}fe#Jhw}N2Gv&Pcl3F_h2vF%c8U&8i+6Uy&K(5df{942 z|EqhNgpdIV9)I*-pmA#-UEVGskQa>^5rAMf-Qk_x#9pK4fvY z3_vc%45_13x6P58m^kDBH0z-N%=90{4|0~sK;Cm~oJS(DKS**pW84-1e1wydMQg4f zqzZw=s)h5?CpxEr(W!Sq=o+fUZg_*RfXddfm5nnjmcA*#N|}~#0Fw1qGZPC&8=QB< zk*J2dTp2(5drhV8d}mZ;`$NAGeoVI~LjEZb6M6b~tM6!m3FX8q0NY6D%}ZF|r$=e1 zhHZb72nG_H*(eR9wIqnz$vGNWzqK|ADRx`xRn;x6)MPEtqssTfz`|EBGCDKx-39&9A47C&s4loqb_1 znKXw#L2SCPJVC0oXE{?+NErKcjfL3l*@Co+b6D&s1FE;le<4PUydpg!jSvsu?SEb< zt=C}x+($A>;L??T4XJ~JYtEHBG1_l?&(nVj@aJTmx)XaF)QuQw$u4xEm;0kUwF2uY zS9Dti+xq4SE&C;V`CessX^@<44cCpIDZgnSJ!mVI5@SFn!Ju(BP!z)|3ZB?ZP5tug z-F@ZTQw91AkZ7m8VgFl+Pk5yW)na(-GmpI`X+SRdqRIClR~kVjm`0)h(=@e&sWR`m zSoTgOSd8}aQU3R-XP~*wkPdu$vL(UTzznax__MaBsp!K)42OWer(9-I=rb228j?oE2Xk-UlC~BRCjf8Hu{YDrH5G^Zt6;7TVSiIs=BT5 z@Ox;A9XFC<%1wUE#mU&+@gj+GKX(lz$4<7*b}sJE&B)VBckA8Ty_YTN##BscKUKn#g!ABk?=kg2T$UTZ8+MG|avsIPlZveAzkhyo} zJSF=z2N0)WdYU1j4w(+3a&PJ)*oxu^zB7%rlM5f*dP6icV$WMMNkhI5qq>Pn_*G(9 z>!ohz+$yMHE>Yh+c9*2NM@qo!gUkzW2re&QJyOhxP?}1Q%kXhEc;bqF2a9<&x1*PE zHdIcY{13dk-0TLnc7O29uJaR4W^xaJoSDarCAc5ZI(-%^r7nShZelvp~r

^xTtKA}4UX)FyCk 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 CCmJFE literal 0 HcmV?d00001 diff --git a/extras/rhubarb-for-spine/src/main/resources/icon-32.png b/extras/rhubarb-for-spine/src/main/resources/icon-32.png new file mode 100644 index 0000000000000000000000000000000000000000..04c0eb812115decbcc250573704fe3d7cf07f342 GIT binary patch literal 942 zcmV;f15x~mP){mTZQ&AK?eJxTbQFLe>N>XY%Ryx|P!*h_OFiIE8(8NT7&&f@V46s-j9P9!e zn1KZcn^0WZ!NlTQOwhIo#!wSXMaqvz^qkAR=JEQsfZB~(MV((KmbChk5HYc_i4<*(b2>xYJ{U(K%GQ{ zRSPi;aOWg(iXQz&Z1{0UGeIb`s8hIL-XiKLs3eabIQ4^KUZM{i=*35xkCDL~8kSI# ze1n!8qb|8zuBBKk7Sc?m4U>>3Nr}Gu{eBo4ia|b~gIq2LwOXypnOWh_GEt~=*v+Gi za=LwqL;_^_CS)?NAeBndvMSnLZg>iClR0h-Zv8)z=5KKgoNJrjzvG@^YXJ?x^7z<66 zVqTQqT3rc|UBj|387!n%P`22A!=fgg)SyL4pA^89h0e>zm367X`zrHfJk{tModO|G z_`Sd++(O~@TyaN9fT#+8eGVtv8PxhVk3 z1&mU8_!Ud*&&|4dJu8Bl%E5M3fHxD1}$sLdEZD)>!9d__?^+M?T&~o z&AO~*05r|zG%rxgy8A-3VP({(506Gs$&cFbSwqLd<1Rf@1JJr%xKq|2m%(El)^8uED_m$5|#;Qf}`P6HIR8${{EPGqzQs!?J4FV zt@*tA!9XtY<5GYJ_Q-)9l1Z8kl;MH-CI~+ywH`M4e#Si3&wPVWpfO`_iyWMn50}T8{l#LDW@cstpi-%1etlN@>b#BI2c=&_ zc%kJ(hYr*7a>>xTqT|Ou(3O>tLHis9EZQ+t6et#p8Gy#e$Hk)8u1$$P0Kxx- zg@O>QP*@nUT8#agg*+hUz;8T3lam*>2@lO{!jo0-+5&)Yvla5X8zqz9(wQ?~Y+D42 zLvR37;D(2{&joM{M!1Gsx_&)P>2z9e$~(oI-==+5{{t(Gj*f`Qb!|exT4~F=TXP|3 zO~Qkwo!++tVEGaz2N1*nECB7+N=OWt@AT-Dp+Y*q$SmqmiZJ@i>`s zwxbKdzm?z>lwg~EoPtmJ21i(yAfiGpV}=T z-6-{XsCOGKZQCIC(RaW7n z#iIU=3SowV5qfub7Yn&(>s<5bKHXZsC!U9Qy-s7VylM0q$BV}+lrH}!o>qrQ)_z|# z0NUIR?C$QKS37ewu$QjxKSFQsJU}7;zgq1Igy_ibH-y&jJ$*<6?FTI_(!ZyRmN(a_ z_~dVMci-K+zppg}08hNcAn?Hw{(9d#^s0ZC?bzs!y|lXdh&G;W3W2Q%7FuKg0IEtR zZd0e17%)EV#776aKqVS%k5n837gKp3H6AZ9)>h46w!(e3!e4Cq_k0{Z2+T?W>_nfh z=7gnCB4q9V+He38#2kYNa}f5xkKqc{Qu)~iee>XF`swi>%5^+%mTg{MVh~w?U`zBA zHdfdY`?m$N6P2E>)7jM@mGCJRK7pbc&-WqEn{RXss3H_LIMhp1NQV? z>#WPiQeE!Iyu?7gsP311+C7_2O}%GpQH%c1^p5}o0KK|AMl98GMF0Q*07*qoM6N<$ Eg0}CK{Qv*} literal 0 HcmV?d00001 From 0cb82f3e4e2a7aa5e88abcfd7edb3aa39a8bf414 Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Sun, 4 Feb 2018 11:17:38 +0100 Subject: [PATCH 16/33] Improved column widths --- .../com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt index 98f24fa..f43822f 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt @@ -76,10 +76,13 @@ class MainView : View() { } fieldset("Audio events") { tableview { - columnResizePolicy = TableView.CONSTRAINED_RESIZE_POLICY + columnResizePolicy = SmartResize.POLICY column("Event", AudioFileModel::eventNameProperty) + .weigthedWidth(1.0) column("Audio file", AudioFileModel::displayFilePathProperty) + .weigthedWidth(1.0) column("Dialog", AudioFileModel::dialogProperty).apply { + weigthedWidth(3.0) // Make dialog column wrap setCellFactory { tableColumn -> return@setCellFactory TableCell().also { cell -> @@ -93,7 +96,9 @@ class MainView : View() { } } column("Status", AudioFileModel::statusLabelProperty) + .weigthedWidth(1.0) column("", AudioFileModel::actionLabelProperty).apply { + weigthedWidth(1.0) // Show button setCellFactory { tableColumn -> return@setCellFactory object : TableCell() { From 389fd0480be9b020ab0787fdec36ad0bfd3e8e97 Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Wed, 7 Feb 2018 20:01:23 +0100 Subject: [PATCH 17/33] Disabling main controls while busy --- .../rhubarb_for_spine/AnimationFileModel.kt | 19 +++++++++++++++++-- .../rhubarb_for_spine/AudioFileModel.kt | 14 ++++++++++++++ .../rhubarb_for_spine/MainView.kt | 5 +++-- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt index 005c784..53f496e 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt @@ -1,5 +1,7 @@ package com.rhubarb_lip_sync.rhubarb_for_spine +import javafx.beans.binding.BooleanBinding +import javafx.beans.property.SimpleBooleanProperty import javafx.beans.property.SimpleListProperty import javafx.beans.property.SimpleObjectProperty import javafx.beans.property.SimpleStringProperty @@ -57,6 +59,21 @@ class AnimationFileModel(animationFilePath: Path, private val executor: Executor } .observable() ) + val audioFileModels by audioFileModelsProperty + + val busyProperty = SimpleBooleanProperty().apply { + bind(object : BooleanBinding() { + init { + for (audioFileModel in audioFileModels) { + super.bind(audioFileModel.busyProperty) + } + } + override fun computeValue(): Boolean { + return audioFileModels.any { it.busy } + } + }) + } + val busy by busyProperty private fun saveAnimation(mouthCues: List, audioEventName: String) { val animationName = getAnimationName(audioEventName) @@ -66,8 +83,6 @@ class AnimationFileModel(animationFilePath: Path, private val executor: Executor private fun getAnimationName(audioEventName: String): String = "say_$audioEventName" - val audioFileModels by audioFileModelsProperty - init { slots = spineJson.slots.observable() mouthSlot = spineJson.guessMouthSlot() diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AudioFileModel.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AudioFileModel.kt index fb4b66e..0bf6076 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AudioFileModel.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AudioFileModel.kt @@ -1,6 +1,7 @@ package com.rhubarb_lip_sync.rhubarb_for_spine import javafx.application.Platform +import javafx.beans.binding.BooleanBinding import javafx.beans.binding.ObjectBinding import javafx.beans.binding.StringBinding import javafx.beans.property.SimpleBooleanProperty @@ -66,6 +67,19 @@ class AudioFileModel( } private val audioFileState by audioFileStateProperty + val busyProperty = SimpleBooleanProperty().apply { + bind(object : BooleanBinding() { + init { + super.bind(futureProperty) + } + override fun computeValue(): Boolean { + return future != null + } + + }) + } + val busy by busyProperty + val statusLabelProperty = SimpleStringProperty().apply { bind(object : StringBinding() { init { diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt index f43822f..76a0823 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt @@ -35,6 +35,7 @@ class MainView : View() { minWidth = 800.0 prefWidth = 1000.0 fieldset("Settings") { + disableProperty().bind(fileModelProperty.select { it!!.busyProperty }) field("Spine JSON file") { filePathTextField = textfield { textProperty().bindBidirectional(mainModel.filePathStringProperty) @@ -123,13 +124,13 @@ class MainView : View() { } onDragOver = EventHandler { event -> - if (event.dragboard.hasFiles()) { + if (event.dragboard.hasFiles() && mainModel.animationFileModel?.busy != true) { event.acceptTransferModes(TransferMode.COPY) event.consume() } } onDragDropped = EventHandler { event -> - if (event.dragboard.hasFiles()) { + if (event.dragboard.hasFiles() && mainModel.animationFileModel?.busy != true) { filePathTextField!!.text = event.dragboard.files.firstOrNull()?.path event.isDropCompleted = true event.consume() From dd1884432c9ba4c98074a89d6ae3fdc1388874b0 Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Wed, 7 Feb 2018 20:38:54 +0100 Subject: [PATCH 18/33] Improved error handling --- .../rhubarb_for_spine/AnimationFileModel.kt | 24 ++++++++++++++++--- .../rhubarb_for_spine/MainView.kt | 8 +++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt index 53f496e..7b2cc63 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt @@ -27,7 +27,7 @@ class AnimationFileModel(animationFilePath: Path, private val executor: Executor mouthShapes = if (mouthSlot != null) { val mouthNames = spineJson.getSlotAttachmentNames(mouthSlot) MouthShape.values().filter { mouthNames.contains(mouthNaming.getName(it)) } - } else null + } else listOf() mouthSlotError = if (mouthSlot != null) null else "No slot with mouth drawings specified." @@ -75,6 +75,19 @@ class AnimationFileModel(animationFilePath: Path, private val executor: Executor } val busy by busyProperty + val validProperty = SimpleBooleanProperty().apply { + val errorProperties = arrayOf(mouthSlotErrorProperty, mouthShapesErrorProperty) + bind(object : BooleanBinding() { + init { + super.bind(*errorProperties) + } + override fun computeValue(): Boolean { + return errorProperties.all { it.value == null } + } + }) + } + val valid by validProperty + private fun saveAnimation(mouthCues: List, audioEventName: String) { val animationName = getAnimationName(audioEventName) spineJson.createOrUpdateAnimation(mouthCues, audioEventName, animationName, mouthSlot, mouthNaming) @@ -94,8 +107,13 @@ class AnimationFileModel(animationFilePath: Path, private val executor: Executor if (missingBasicShapes.isEmpty()) return null val result = StringBuilder() - result.append("Mouth shapes ${missingBasicShapes.joinToString()}") - result.appendln(if (missingBasicShapes.count() > 1) " are missing." else " is missing.") + val missingShapesString = missingBasicShapes.joinToString() + result.appendln( + if (missingBasicShapes.count() > 1) + "Mouth shapes $missingShapesString are missing." + else + "Mouth shape $missingShapesString is missing." + ) val first = MouthShape.basicShapes.first() val last = MouthShape.basicShapes.last() diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt index 76a0823..b797d13 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt @@ -1,5 +1,8 @@ package com.rhubarb_lip_sync.rhubarb_for_spine +import javafx.beans.binding.Bindings +import javafx.beans.property.Property +import javafx.beans.property.SimpleBooleanProperty import javafx.beans.property.SimpleStringProperty import javafx.event.ActionEvent import javafx.event.EventHandler @@ -77,6 +80,7 @@ class MainView : View() { } fieldset("Audio events") { tableview { + placeholder = Label("There are no events with associated audio files.") columnResizePolicy = SmartResize.POLICY column("Event", AudioFileModel::eventNameProperty) .weigthedWidth(1.0) @@ -112,6 +116,10 @@ class MainView : View() { val audioFileModel = this@tableview.items[index] audioFileModel.performAction() } + val invalidProperty: Property = fileModelProperty + .select { it!!.validProperty } + .select { SimpleBooleanProperty(!it) } + disableProperty().bind(invalidProperty) } else null From f3de163d720a489271fc1bce6f9fcf815dd64876 Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Wed, 7 Feb 2018 20:49:35 +0100 Subject: [PATCH 19/33] Improved error message formatting --- .../com/rhubarb_lip_sync/rhubarb_for_spine/AudioFileModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AudioFileModel.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AudioFileModel.kt index 0bf6076..9220f08 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AudioFileModel.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AudioFileModel.kt @@ -145,8 +145,8 @@ class AudioFileModel( } catch (e: Exception) { Platform.runLater { Alert(Alert.AlertType.ERROR).apply { - headerText = "Error performing lip-sync for event $eventName." - contentText = e.toString() + headerText = "Error performing lip-sync for event '$eventName'." + contentText = e.message show() } } From 8f1056dc5f4236758b6f11e8c1442d650af99b5c Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Wed, 7 Feb 2018 20:50:35 +0100 Subject: [PATCH 20/33] Normalizing paths This doesn't affect behavior but makes for nicer error messages. --- .../com/rhubarb_lip_sync/rhubarb_for_spine/SpineJson.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/SpineJson.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/SpineJson.kt index 91edb5f..12f784e 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/SpineJson.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/SpineJson.kt @@ -36,7 +36,7 @@ class SpineJson(val filePath: Path) { ?: throw Exception("JSON file is incomplete: Images path is missing." + "Make sure to check 'Nonessential data' when exporting.") - val imagesDirectoryPath = fileDirectoryPath.resolve(relativeImagesDirectory) + val imagesDirectoryPath = fileDirectoryPath.resolve(relativeImagesDirectory).normalize() if (!Files.exists(imagesDirectoryPath)) { throw Exception("Could not find images directory relative to the JSON file." + " Make sure the JSON file is in the same directory as the original Spine file.") @@ -50,7 +50,7 @@ class SpineJson(val filePath: Path) { ?: throw Exception("JSON file is incomplete: Audio path is missing." + "Make sure to check 'Nonessential data' when exporting.") - val audioDirectoryPath = fileDirectoryPath.resolve(relativeAudioDirectory) + val audioDirectoryPath = fileDirectoryPath.resolve(relativeAudioDirectory).normalize() if (!Files.exists(audioDirectoryPath)) { throw Exception("Could not find audio directory relative to the JSON file." + " Make sure the JSON file is in the same directory as the original Spine file.") From 7fcaa2ddf70247be82e0a2151c83581675110896 Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Thu, 8 Feb 2018 10:07:52 +0100 Subject: [PATCH 21/33] Setting dock icon on OS X --- .../rhubarb_for_spine/MainApp.kt | 39 ++++++++++++++++++- .../rhubarb_for_spine/MainView.kt | 6 --- 2 files changed, 38 insertions(+), 7 deletions(-) mode change 100644 => 100755 extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainApp.kt diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainApp.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainApp.kt old mode 100644 new mode 100755 index 0d21765..d82c65e --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainApp.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainApp.kt @@ -1,5 +1,42 @@ package com.rhubarb_lip_sync.rhubarb_for_spine +import javafx.scene.image.Image +import javafx.stage.Stage import tornadofx.App +import tornadofx.addStageIcon +import java.lang.reflect.Method +import javax.swing.ImageIcon -class MainApp : App(MainView::class) +class MainApp : App(MainView::class) { + override fun start(stage: Stage) { + super.start(stage) + setIcon() + } + + private fun setIcon() { + // Set icon for windows + for (iconSize in listOf(16, 32, 48, 256)) { + addStageIcon(Image(this.javaClass.getResourceAsStream("/icon-$iconSize.png"))) + } + + // OS X requires the dock icon to be changed separately. + // Not all JDKs contain the class com.apple.eawt.Application, so we have to use reflection. + val classLoader = this.javaClass.classLoader + try { + val iconURL = this.javaClass.getResource("/icon-256.png") + val image: java.awt.Image = ImageIcon(iconURL).image + + // The following is reflection code for the line + // Application.getApplication().setDockIconImage(image) + val applicationClass: Class<*> = classLoader.loadClass("com.apple.eawt.Application") + val getApplicationMethod: Method = applicationClass.getMethod("getApplication") + val application: Any = getApplicationMethod.invoke(null) + val setDockIconImageMethod: Method = + applicationClass.getMethod("setDockIconImage", java.awt.Image::class.java) + setDockIconImageMethod.invoke(application, image); + } catch (e: Exception) { + // Works only on OS X + } + } + +} diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt index b797d13..56bf74e 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt @@ -7,7 +7,6 @@ import javafx.beans.property.SimpleStringProperty import javafx.event.ActionEvent import javafx.event.EventHandler import javafx.scene.control.* -import javafx.scene.image.Image import javafx.scene.input.DragEvent import javafx.scene.input.TransferMode import javafx.scene.text.Text @@ -22,11 +21,6 @@ class MainView : View() { init { title = "Rhubarb Lip Sync for Spine" - - // Set icon - for (iconSize in listOf(16, 32, 48, 256)) { - addStageIcon(Image(this.javaClass.getResourceAsStream("/icon-$iconSize.png"))) - } } override val root = form { From ce54ba60a7c95ab793e1342a15671155d4091d15 Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Thu, 8 Feb 2018 10:08:43 +0100 Subject: [PATCH 22/33] Respecting table cell padding --- .../com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt index 56bf74e..1ca39e8 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt @@ -88,7 +88,10 @@ class MainView : View() { cell.graphic = Text().apply { textProperty().bind(cell.itemProperty()) fillProperty().bind(cell.textFillProperty()) - wrappingWidthProperty().bind(tableColumn.widthProperty()) + val widthProperty = tableColumn.widthProperty() + .minus(cell.paddingLeftProperty) + .minus(cell.paddingRightProperty) + wrappingWidthProperty().bind(widthProperty) } cell.prefHeight = Control.USE_COMPUTED_SIZE } From 5db03da56f030773d92a057d7a1ad7381a5c24a6 Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Fri, 9 Feb 2018 22:18:48 +0100 Subject: [PATCH 23/33] Allowing the animation naming to be customized --- .../rhubarb_for_spine/AnimationFileModel.kt | 13 +++---- .../rhubarb_for_spine/AudioFileModel.kt | 35 ++++++++++++++++--- .../rhubarb_for_spine/MainModel.kt | 8 ++++- .../rhubarb_for_spine/MainView.kt | 13 +++++++ .../rhubarb_for_spine/SpineJson.kt | 10 +++--- 5 files changed, 63 insertions(+), 16 deletions(-) diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt index 7b2cc63..630b055 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt @@ -12,7 +12,7 @@ import tornadofx.observable import tornadofx.setValue import java.util.concurrent.ExecutorService -class AnimationFileModel(animationFilePath: Path, private val executor: ExecutorService) { +class AnimationFileModel(val parentModel: MainModel, animationFilePath: Path, private val executor: ExecutorService) { val spineJson = SpineJson(animationFilePath) val slotsProperty = SimpleObjectProperty>() @@ -55,7 +55,11 @@ class AnimationFileModel(animationFilePath: Path, private val executor: Executor val audioFileModelsProperty = SimpleListProperty( spineJson.audioEvents .map { event -> - AudioFileModel(event, this, executor, { result -> saveAnimation(result, event.name) }) + var audioFileModel: AudioFileModel? = null + val reportResult: (List) -> Unit = + { result -> saveAnimation(audioFileModel!!.animationName, event.name, result) } + audioFileModel = AudioFileModel(event, this, executor, reportResult) + return@map audioFileModel } .observable() ) @@ -88,14 +92,11 @@ class AnimationFileModel(animationFilePath: Path, private val executor: Executor } val valid by validProperty - private fun saveAnimation(mouthCues: List, audioEventName: String) { - val animationName = getAnimationName(audioEventName) + private fun saveAnimation(animationName: String, audioEventName: String, mouthCues: List) { spineJson.createOrUpdateAnimation(mouthCues, audioEventName, animationName, mouthSlot, mouthNaming) spineJson.save() } - private fun getAnimationName(audioEventName: String): String = "say_$audioEventName" - init { slots = spineJson.slots.observable() mouthSlot = spineJson.guessMouthSlot() diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AudioFileModel.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AudioFileModel.kt index 9220f08..11b81c2 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AudioFileModel.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AudioFileModel.kt @@ -29,6 +29,23 @@ class AudioFileModel( val displayFilePathProperty = SimpleStringProperty(audioEvent.relativeAudioFilePath) val displayFilePath by displayFilePathProperty + val animationNameProperty = SimpleStringProperty().apply { + val mainModel = parentModel.parentModel + bind(object : ObjectBinding() { + init { + super.bind( + mainModel.animationPrefixProperty, + eventNameProperty, + mainModel.animationSuffixProperty + ) + } + override fun computeValue(): String { + return mainModel.animationPrefix + eventName + mainModel.animationSuffix + } + }) + } + val animationName by animationNameProperty + val dialogProperty = SimpleStringProperty(audioEvent.dialog) val dialog: String? by dialogProperty @@ -36,8 +53,17 @@ class AudioFileModel( var animationProgress by animationProgressProperty private set - private val animatedPreviouslyProperty = SimpleBooleanProperty(false) // TODO: Initial value - private var animatedPreviously by animatedPreviouslyProperty + private val animatedProperty = SimpleBooleanProperty().apply { + bind(object : ObjectBinding() { + init { + super.bind(animationNameProperty, parentModel.spineJson.animationNames) + } + override fun computeValue(): Boolean { + return parentModel.spineJson.animationNames.contains(animationName) + } + }) + } + private var animated by animatedProperty private val futureProperty = SimpleObjectProperty?>() private var future by futureProperty @@ -45,7 +71,7 @@ class AudioFileModel( private val audioFileStateProperty = SimpleObjectProperty().apply { bind(object : ObjectBinding() { init { - super.bind(animatedPreviouslyProperty, futureProperty, animationProgressProperty) + super.bind(animatedProperty, futureProperty, animationProgressProperty) } override fun computeValue(): AudioFileState { return if (future != null) { @@ -57,7 +83,7 @@ class AudioFileModel( else AudioFileState(AudioFileStatus.Pending) } else { - if (animatedPreviously) + if (animated) AudioFileState(AudioFileStatus.Done) else AudioFileState(AudioFileStatus.NotAnimated) @@ -133,7 +159,6 @@ class AudioFileModel( val result = rhubarbTask.call() runAndWait { reportResult(result) - animatedPreviously = true } } finally { runAndWait { diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainModel.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainModel.kt index f860a44..ffe55e9 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainModel.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainModel.kt @@ -29,7 +29,7 @@ class MainModel(private val executor: ExecutorService) { throw Exception("File does not exist.") } - animationFileModel = AnimationFileModel(path, executor) + animationFileModel = AnimationFileModel(this, path, executor) } } var filePathString by filePathStringProperty @@ -42,5 +42,11 @@ class MainModel(private val executor: ExecutorService) { var animationFileModel by animationFileModelProperty private set + val animationPrefixProperty = SimpleStringProperty("say_") + var animationPrefix by animationPrefixProperty + + val animationSuffixProperty = SimpleStringProperty("") + var animationSuffix by animationSuffixProperty + private fun getDefaultPathString() = FX.application.parameters.raw.firstOrNull() } \ No newline at end of file diff --git a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt index 1ca39e8..cc4ab47 100644 --- a/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt +++ b/extras/rhubarb-for-spine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt @@ -71,6 +71,17 @@ class MainView : View() { errorProperty().bind(fileModelProperty.select { it!!.mouthShapesErrorProperty }) } } + field("Animation naming") { + textfield { + maxWidth = 100.0 + textProperty().bindBidirectional(mainModel.animationPrefixProperty) + } + label("

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 literal 0 HcmV?d00001