From 12293d3cf810a5fe1482c7894fda4a81f8c9a385 Mon Sep 17 00:00:00 2001 From: Andy McFadden Date: Sat, 21 Dec 2019 18:04:44 -0800 Subject: [PATCH] More progress on animated visualizations Bitmap animations are composed of a sequence of other visualizations. This is all well and good until a visualization is deleted, at which point all animations in all sets in the entire project have to be checked and potentially changed, and perhaps even removed (if all of the animation's members have been removed). This turns out to be kind of annoying to deal with, but it's better to deal with it in code than force the user to manually update broken animations. This change adds thumbnails for the animations, currently generated by offscreen composition. This approach doesn't work quite right. --- ImageSrc/BlueChevron.xcf | Bin 0 -> 2421 bytes SourceGen/MainController.cs | 26 +++- SourceGen/Res/BlueChevron.png | Bin 0 -> 376 bytes SourceGen/SourceGen.csproj | 3 + SourceGen/Visualization.cs | 12 ++ SourceGen/VisualizationAnimation.cs | 136 +++++++++++++++++- SourceGen/VisualizationSet.cs | 77 +++++++++- SourceGen/WpfGui/EditBitmapAnimation.xaml.cs | 22 ++- SourceGen/WpfGui/EditVisualizationSet.xaml.cs | 44 ++++++ 9 files changed, 307 insertions(+), 13 deletions(-) create mode 100644 ImageSrc/BlueChevron.xcf create mode 100644 SourceGen/Res/BlueChevron.png diff --git a/ImageSrc/BlueChevron.xcf b/ImageSrc/BlueChevron.xcf new file mode 100644 index 0000000000000000000000000000000000000000..c1732ab4f7bf7663c37ebcb33fdfcd70d56ea9e3 GIT binary patch literal 2421 zcmeHF%SyvQ6rIH2qg8x&?7JlR5GH-W`8(I{cwluQNnLg7^!t3>XHMRbciE5&^LWSbzzX zB&WW0^q(Uyc-Xq?guR{{B&ZoZTsezeJ8|3m(NAvp`NicGJy{a1e$V#Ysu%ff($HfJDwAE!-$tfsz%XaXrO@b8d}lj; z;PK-l`3i{r-$bcZ?*l*Ke&F1=G0lgaPV6Sb1Jd+QDN{yA=^nH&-n2r|1_*&}w|Hth zw_X(92jU9J8H#CuoJxUB;wR5o;U+*jjm}Aq#I`iPq49#oxyFkcpV#<;#uvfqeJW-^ z=-sscuH;B;hj@xW-qyJOw?S56zAJ{@kf41L=4Y10+Q3Rcm|t0DY+!savo+7m(gB-l z71>m2U*^2FS_NiJfIfQuiu{fW%g&$IS>L$n>fZOI9|PkxbEgS+DsZC-cPi)-Ij^lI z+?fD<^!#u`d kvp in mProject.VisualizationSets) { + if (kvp.Value == curVisSet) { + continue; + } + + VisualizationSet stripSet; + if (VisualizationSet.StripEntriesFromAnimations(kvp.Value, dlg.RemovedSerials, + out stripSet)) { + if (stripSet.Count == 0) { + stripSet = null; + } + uc = UndoableChange.CreateVisualizationSetChange(kvp.Key, + kvp.Value, stripSet); + cs.Add(uc); + Debug.WriteLine("Also updating visSet at +" + kvp.Key.ToString("x6")); + } + } + ApplyUndoableChanges(cs); } else { Debug.WriteLine("No change to VisualizationSet"); diff --git a/SourceGen/Res/BlueChevron.png b/SourceGen/Res/BlueChevron.png new file mode 100644 index 0000000000000000000000000000000000000000..4c41364d83fa9e9f002f84488c3f0446e4c6cb96 GIT binary patch literal 376 zcmV-;0f+vHP)HK~#9!?V7O_gFp;JF+*gC?2#_xrpq1~B1@1eFFalv;A~4zQbhxW ztbg43JWZBmS(YWMIP>!Twk+T0$1`?+>vH|Q|9(Yu7%-3e(Atb6TAM*>fFuI{D!&(n zZRM*`7@hz&6iyp(Wbx~)_;3W+QAk?^Sx^{e04oZSU3qjA()R)AC=54%4TY$xJSqxh zTVYfb(hOihA*w5nhC=yi5DkTN1JF^3uF9jJ(AE$^L7~h5R1~7S^4%!3_XN99C^rBN zg+Qr%7Yg!iao-Cl$KPiG1O&SO3ZWYP@&w3rgjhC2p&I?N1jyYB_$)vY{qzLL-43%w zKNSI{8)DB0sYX8q0meBYPl~EWe<%XX)1tF#^wSWKnh|0jlx}T^ph$jF)UqtgQq~8w WQ~+h|c!Br;0000 + + + \ No newline at end of file diff --git a/SourceGen/Visualization.cs b/SourceGen/Visualization.cs index d67db5b..c6c85db 100644 --- a/SourceGen/Visualization.cs +++ b/SourceGen/Visualization.cs @@ -72,12 +72,24 @@ namespace SourceGen { /// public BitmapSource CachedImage { get; set; } + /// + /// True if CachedImage has something other than the default value. + /// + public bool HasImage { + get { + return CachedImage != BROKEN_IMAGE && CachedImage != ANIM_IMAGE; + } + } + /// /// Image to show when things are broken. /// public static readonly BitmapImage BROKEN_IMAGE = new BitmapImage(new Uri("pack://application:,,,/Res/RedX.png")); + internal static readonly BitmapImage ANIM_IMAGE = + new BitmapImage(new Uri("pack://application:,,,/Res/BlueChevron.png")); + /// /// Serial number, for reference from other Visualization objects. Not serialized. /// diff --git a/SourceGen/VisualizationAnimation.cs b/SourceGen/VisualizationAnimation.cs index 237a0ca..1d68383 100644 --- a/SourceGen/VisualizationAnimation.cs +++ b/SourceGen/VisualizationAnimation.cs @@ -18,6 +18,9 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Text; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Imaging; namespace SourceGen { /// @@ -27,6 +30,8 @@ namespace SourceGen { /// References to Visualization objects (such as a 3D mesh or list of bitmaps) are held /// here. The VisGenParams property holds animation properties, such as frame rate and /// view angles. + /// + /// As with the base class, instances are generally immutable for the benefit of undo/redo. /// public class VisualizationAnimation : Visualization { /// @@ -64,11 +69,67 @@ namespace SourceGen { /// Parameters for visualization generator. /// Serial numbers of referenced Visualizations. public VisualizationAnimation(string tag, string visGenIdent, - ReadOnlyDictionary visGenParams, List visSerialNumbers) - : base(tag, visGenIdent, visGenParams) { + ReadOnlyDictionary visGenParams, List visSerialNumbers, + VisualizationAnimation oldObj) + : base(tag, visGenIdent, visGenParams, oldObj) { Debug.Assert(visSerialNumbers != null); - mSerialNumbers = visSerialNumbers; + // Make a copy of the list. + mSerialNumbers = new List(visSerialNumbers.Count); + foreach (int serial in visSerialNumbers) { + mSerialNumbers.Add(serial); + } + + CachedImage = ANIM_IMAGE; // default to this + } + + /// + /// The number of Visualizations linked from this animation. + /// + public int SerialCount { + get { return mSerialNumbers.Count; } + } + + public void GenerateImage(SortedList visSets) { + const int IMAGE_SIZE = 64; + + CachedImage = ANIM_IMAGE; + + if (mSerialNumbers.Count == 0) { + return; + } + Visualization vis = VisualizationSet.FindVisualizationBySerial(visSets, + mSerialNumbers[0]); + if (vis == null) { + return; + } + + double maxDim = Math.Max(vis.CachedImage.Width, vis.CachedImage.Height); + double dimMult = IMAGE_SIZE / maxDim; + double adjWidth = vis.CachedImage.Width * dimMult; + double adjHeight = vis.CachedImage.Height * dimMult; + Rect imgBounds = new Rect((IMAGE_SIZE - adjWidth) / 2, (IMAGE_SIZE - adjHeight) / 2, + adjWidth, adjHeight); + + DrawingVisual visual = new DrawingVisual(); + //RenderOptions.SetBitmapScalingMode(visual, BitmapScalingMode.NearestNeighbor); + DrawingContext dc = visual.RenderOpen(); + dc.DrawImage(vis.CachedImage, imgBounds); + dc.DrawImage(ANIM_IMAGE, new Rect(0, 0, IMAGE_SIZE, IMAGE_SIZE)); + dc.Close(); + + RenderTargetBitmap bmp = new RenderTargetBitmap(IMAGE_SIZE, IMAGE_SIZE, 96.0, 96.0, + PixelFormats.Pbgra32); + bmp.Render(visual); + CachedImage = bmp; + Debug.WriteLine("RENDERED " + Tag); + } + + /// + /// Returns a list of serial numbers. The caller must not modify the list. + /// + public List GetSerialNumbers() { + return mSerialNumbers; } /// @@ -83,5 +144,74 @@ namespace SourceGen { } return false; } + + /// + /// Strips serial numbers out of the list. + /// + /// Object to strip serial numbers from. + /// List of serial numbers to remove. + /// Object with changes, or null if nothing changed. + /// True if something was actually removed. + public static bool StripEntries(VisualizationAnimation visAnim, List removedSerials, + out VisualizationAnimation newAnim) { + bool somethingRemoved = false; + + // Both sets should be small, so not worried about O(m*n). + List newSerials = new List(visAnim.mSerialNumbers.Count); + foreach (int serial in visAnim.mSerialNumbers) { + if (removedSerials.Contains(serial)) { + Debug.WriteLine("Removing serial #" + serial + " from " + visAnim.Tag); + somethingRemoved = true; + continue; + } + newSerials.Add(serial); + } + + if (somethingRemoved) { + newAnim = new VisualizationAnimation(visAnim.Tag, visAnim.VisGenIdent, + visAnim.VisGenParams, newSerials, visAnim); + } else { + newAnim = null; + } + return somethingRemoved; + } + + + public static bool operator ==(VisualizationAnimation a, VisualizationAnimation b) { + if (ReferenceEquals(a, b)) { + return true; // same object, or both null + } + if (ReferenceEquals(a, null) || ReferenceEquals(b, null)) { + return false; // one is null + } + return a.Equals(b); + } + public static bool operator !=(VisualizationAnimation a, VisualizationAnimation b) { + return !(a == b); + } + public override bool Equals(object obj) { + if (!(obj is VisualizationAnimation)) { + return false; + } + // Do base-class equality comparison and the ReferenceEquals check. + if (!base.Equals(obj)) { + return false; + } + Debug.WriteLine("Detailed: this=" + Tag + " other=" + Tag); + VisualizationAnimation other = (VisualizationAnimation)obj; + if (other.mSerialNumbers.Count != mSerialNumbers.Count) { + return false; + } + for (int i = 0; i < mSerialNumbers.Count; i++) { + if (other.mSerialNumbers[i] != mSerialNumbers[i]) { + return false; + } + } + Debug.WriteLine(" All serial numbers match"); + return true; + } + public override int GetHashCode() { + return base.GetHashCode() ^ mSerialNumbers.Count; // weak + } } } diff --git a/SourceGen/VisualizationSet.cs b/SourceGen/VisualizationSet.cs index 999d9db..dd21125 100644 --- a/SourceGen/VisualizationSet.cs +++ b/SourceGen/VisualizationSet.cs @@ -86,6 +86,56 @@ namespace SourceGen { return arr; } + /// + /// Finds a Visualization by serial number. + /// + /// List of sets of visualizations. + /// Serial number to search for. + /// Matching Visualization, or null if not found. + public static Visualization FindVisualizationBySerial( + SortedList visSets, int serial) { + foreach (KeyValuePair kvp in visSets) { + VisualizationSet visSet = kvp.Value; + foreach (Visualization vis in visSet) { + if (vis.SerialNumber == serial) { + return vis; + } + } + } + return null; + } + + /// + /// Strips a set of Visualizations out of animations in the set. The set is not + /// modified; rather, a new set is generated with updated entries. + /// + /// Input set. + /// Serial numbers of removed items. + /// Updated set. + /// True if entries were removed. + public static bool StripEntriesFromAnimations(VisualizationSet visSet, + List removedSerials, out VisualizationSet newSet) { + bool somethingRemoved = false; + newSet = new VisualizationSet(visSet.Count); + foreach (Visualization vis in visSet) { + if (!(vis is VisualizationAnimation)) { + newSet.Add(vis); + continue; + } + + if (VisualizationAnimation.StripEntries((VisualizationAnimation) vis, + removedSerials, out VisualizationAnimation newAnim)) { + somethingRemoved = true; + if (newAnim.SerialCount != 0) { + newSet.Add(newAnim); + } else { + Debug.WriteLine("Deleting empty animation " + vis.Tag); + } + } + } + return somethingRemoved; + } + #region Image generation private class ScriptSupport : MarshalByRefObject, PluginCommon.IApplication { @@ -126,14 +176,20 @@ namespace SourceGen { ScriptSupport iapp = null; List plugins = null; - foreach (KeyValuePair kvp in project.VisualizationSets) { + SortedList visSets = project.VisualizationSets; + + foreach (KeyValuePair kvp in visSets) { VisualizationSet visSet = kvp.Value; foreach (Visualization vis in visSet) { - if (vis.CachedImage != Visualization.BROKEN_IMAGE) { + if (vis.HasImage) { continue; } //Debug.WriteLine("Vis needs refresh: " + vis.Tag); + if (vis is VisualizationAnimation) { + continue; + } + if (iapp == null) { // Prep the plugins on first need. iapp = new ScriptSupport(); @@ -171,6 +227,19 @@ namespace SourceGen { if (iapp != null) { project.UnprepareScripts(); } + + // Now that we've generated images for the Visualizations, update any + // VisualizationAnimation thumbnails that may have been affected. + foreach (KeyValuePair kvp in visSets) { + VisualizationSet visSet = kvp.Value; + foreach (Visualization vis in visSet) { + if (!(vis is VisualizationAnimation)) { + continue; + } + VisualizationAnimation visAnim = (VisualizationAnimation)vis; + visAnim.GenerateImage(visSets); + } + } } /// @@ -215,9 +284,9 @@ namespace SourceGen { if (a.mList.Count != b.mList.Count) { return false; } - // Order matters. + // Order matters. Use Equals() rather than == to get polymorphism. for (int i = 0; i < a.mList.Count; i++) { - if (a.mList[i] != b.mList[i]) { + if (!a.mList[i].Equals(b.mList[i])) { return false; } } diff --git a/SourceGen/WpfGui/EditBitmapAnimation.xaml.cs b/SourceGen/WpfGui/EditBitmapAnimation.xaml.cs index a4d5d66..586a0b8 100644 --- a/SourceGen/WpfGui/EditBitmapAnimation.xaml.cs +++ b/SourceGen/WpfGui/EditBitmapAnimation.xaml.cs @@ -158,15 +158,27 @@ namespace SourceGen.WpfGui { } private void PopulateItemLists() { + // Add the animation's visualizations, in order. + if (mOrigAnim != null) { + foreach (int serial in mOrigAnim.GetSerialNumbers()) { + Visualization vis = VisualizationSet.FindVisualizationBySerial(mEditedList, + serial); + if (vis != null) { + VisAnimItems.Add(vis); + } else { + Debug.Assert(false); + } + } + } + + // Add all remaining non-animation Visualizations to the "source" set. foreach (KeyValuePair kvp in mEditedList) { foreach (Visualization vis in kvp.Value) { if (vis is VisualizationAnimation) { - // disallow animations with animations + // disallow using animations as animation frames continue; } - if (mOrigAnim != null && mOrigAnim.ContainsSerial(vis.SerialNumber)) { - VisAnimItems.Add(vis); - } else { + if (!VisAnimItems.Contains(vis)) { VisSourceItems.Add(vis); } } @@ -194,7 +206,7 @@ namespace SourceGen.WpfGui { } NewAnim = new VisualizationAnimation(TagString, VisualizationAnimation.ANIM_VIS_GEN, - new ReadOnlyDictionary(visGenParams), serials); + new ReadOnlyDictionary(visGenParams), serials, mOrigAnim); DialogResult = true; } diff --git a/SourceGen/WpfGui/EditVisualizationSet.xaml.cs b/SourceGen/WpfGui/EditVisualizationSet.xaml.cs index 7d9490c..9a087b0 100644 --- a/SourceGen/WpfGui/EditVisualizationSet.xaml.cs +++ b/SourceGen/WpfGui/EditVisualizationSet.xaml.cs @@ -36,11 +36,24 @@ namespace SourceGen.WpfGui { /// public VisualizationSet NewVisSet { get; private set; } + /// + /// List of Visualization serial numbers that were removed from the set. The caller + /// can use this to update animations in other sets that referred to the removed items. + /// + /// + /// We have to use serial numbers because the user might have edited the Visualization + /// before removing it. + /// + public List RemovedSerials { get; private set; } + private DisasmProject mProject; private Formatter mFormatter; private VisualizationSet mOrigSet; private int mOffset; + /// + /// ItemsSource for visualizationGrid. + /// public ObservableCollection VisualizationList { get; private set; } = new ObservableCollection(); @@ -99,6 +112,8 @@ namespace SourceGen.WpfGui { mOrigSet = curSet; mOffset = offset; + RemovedSerials = new List(); + if (curSet != null) { // Populate the data grid ItemsSource. foreach (Visualization vis in curSet) { @@ -222,6 +237,35 @@ namespace SourceGen.WpfGui { if (index >= 0) { visualizationGrid.SelectedIndex = index; } + + RemovedSerials.Add(item.SerialNumber); + + // Update any animations in this set. Animations in other sets will be updated later. + // (This is a bit awkward because we can't modify VisualizationList while iterating + // through it, and there's no simple "replace entry" operation on an observable + // collection. Fortunately we don't do this often and the data sets are small.) + List needsUpdate = new List(); + foreach (Visualization vis in VisualizationList) { + if (vis is VisualizationAnimation) { + VisualizationAnimation visAnim = (VisualizationAnimation)vis; + if (visAnim.ContainsSerial(item.SerialNumber)) { + needsUpdate.Add(visAnim); + } + } + } + foreach (VisualizationAnimation visAnim in needsUpdate) { + VisualizationAnimation newAnim; + if (VisualizationAnimation.StripEntries(visAnim, + new List(1) { item.SerialNumber }, out newAnim)) { + if (newAnim.SerialCount == 0) { + VisualizationList.Remove(visAnim); + } else { + index = VisualizationList.IndexOf(visAnim); + VisualizationList.Remove(visAnim); + VisualizationList.Insert(index, newAnim); + } + } + } } private void UpButton_Click(object sender, RoutedEventArgs e) {