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 0000000..0e9923f Binary files /dev/null and b/extras/rhubarb-for-spine/rhubarb-for-spine/rhubarb.ico differ