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.
This commit is contained in:
Andy McFadden 2019-12-21 18:04:44 -08:00
parent fef7668b63
commit 12293d3cf8
9 changed files with 307 additions and 13 deletions

BIN
ImageSrc/BlueChevron.xcf Normal file

Binary file not shown.

View File

@ -2179,11 +2179,35 @@ namespace SourceGen {
newSet = null;
}
if (curVisSet != newSet) {
ChangeSet cs = new ChangeSet(1);
// New table, edited in place, or deleted.
UndoableChange uc = UndoableChange.CreateVisualizationSetChange(offset,
curVisSet, newSet);
//Debug.WriteLine("Change " + curVisSet + " to " + newSet);
ChangeSet cs = new ChangeSet(uc);
cs.Add(uc);
// And now the messy bit. If Visualizations were removed, we need to purge
// them from any animations that reference them. The edit dialog took care
// of this for animations in the same set, but we need to check other sets.
foreach (KeyValuePair<int, VisualizationSet> 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");

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 B

View File

@ -430,5 +430,8 @@
<ItemGroup>
<Resource Include="Res\RedX.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Res\BlueChevron.png" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -72,12 +72,24 @@ namespace SourceGen {
/// </remarks>
public BitmapSource CachedImage { get; set; }
/// <summary>
/// True if CachedImage has something other than the default value.
/// </summary>
public bool HasImage {
get {
return CachedImage != BROKEN_IMAGE && CachedImage != ANIM_IMAGE;
}
}
/// <summary>
/// Image to show when things are broken.
/// </summary>
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"));
/// <summary>
/// Serial number, for reference from other Visualization objects. Not serialized.
/// </summary>

View File

@ -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 {
/// <summary>
@ -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.
/// </remarks>
public class VisualizationAnimation : Visualization {
/// <summary>
@ -64,11 +69,67 @@ namespace SourceGen {
/// <param name="visGenParams">Parameters for visualization generator.</param>
/// <param name="visSerialNumbers">Serial numbers of referenced Visualizations.</param>
public VisualizationAnimation(string tag, string visGenIdent,
ReadOnlyDictionary<string, object> visGenParams, List<int> visSerialNumbers)
: base(tag, visGenIdent, visGenParams) {
ReadOnlyDictionary<string, object> visGenParams, List<int> visSerialNumbers,
VisualizationAnimation oldObj)
: base(tag, visGenIdent, visGenParams, oldObj) {
Debug.Assert(visSerialNumbers != null);
mSerialNumbers = visSerialNumbers;
// Make a copy of the list.
mSerialNumbers = new List<int>(visSerialNumbers.Count);
foreach (int serial in visSerialNumbers) {
mSerialNumbers.Add(serial);
}
CachedImage = ANIM_IMAGE; // default to this
}
/// <summary>
/// The number of Visualizations linked from this animation.
/// </summary>
public int SerialCount {
get { return mSerialNumbers.Count; }
}
public void GenerateImage(SortedList<int, VisualizationSet> 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);
}
/// <summary>
/// Returns a list of serial numbers. The caller must not modify the list.
/// </summary>
public List<int> GetSerialNumbers() {
return mSerialNumbers;
}
/// <summary>
@ -83,5 +144,74 @@ namespace SourceGen {
}
return false;
}
/// <summary>
/// Strips serial numbers out of the list.
/// </summary>
/// <param name="visAnim">Object to strip serial numbers from.</param>
/// <param name="removedSerials">List of serial numbers to remove.</param>
/// <param name="newAnim">Object with changes, or null if nothing changed.</param>
/// <returns>True if something was actually removed.</returns>
public static bool StripEntries(VisualizationAnimation visAnim, List<int> removedSerials,
out VisualizationAnimation newAnim) {
bool somethingRemoved = false;
// Both sets should be small, so not worried about O(m*n).
List<int> newSerials = new List<int>(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
}
}
}

View File

@ -86,6 +86,56 @@ namespace SourceGen {
return arr;
}
/// <summary>
/// Finds a Visualization by serial number.
/// </summary>
/// <param name="visSets">List of sets of visualizations.</param>
/// <param name="serial">Serial number to search for.</param>
/// <returns>Matching Visualization, or null if not found.</returns>
public static Visualization FindVisualizationBySerial(
SortedList<int, VisualizationSet> visSets, int serial) {
foreach (KeyValuePair<int, VisualizationSet> kvp in visSets) {
VisualizationSet visSet = kvp.Value;
foreach (Visualization vis in visSet) {
if (vis.SerialNumber == serial) {
return vis;
}
}
}
return null;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="visSet">Input set.</param>
/// <param name="removedSerials">Serial numbers of removed items.</param>
/// <param name="newSet">Updated set.</param>
/// <returns>True if entries were removed.</returns>
public static bool StripEntriesFromAnimations(VisualizationSet visSet,
List<int> 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<IPlugin> plugins = null;
foreach (KeyValuePair<int, VisualizationSet> kvp in project.VisualizationSets) {
SortedList<int, VisualizationSet> visSets = project.VisualizationSets;
foreach (KeyValuePair<int, VisualizationSet> 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<int, VisualizationSet> kvp in visSets) {
VisualizationSet visSet = kvp.Value;
foreach (Visualization vis in visSet) {
if (!(vis is VisualizationAnimation)) {
continue;
}
VisualizationAnimation visAnim = (VisualizationAnimation)vis;
visAnim.GenerateImage(visSets);
}
}
}
/// <summary>
@ -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;
}
}

View File

@ -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<int, VisualizationSet> 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<string, object>(visGenParams), serials);
new ReadOnlyDictionary<string, object>(visGenParams), serials, mOrigAnim);
DialogResult = true;
}

View File

@ -36,11 +36,24 @@ namespace SourceGen.WpfGui {
/// </summary>
public VisualizationSet NewVisSet { get; private set; }
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// We have to use serial numbers because the user might have edited the Visualization
/// before removing it.
/// </remarks>
public List<int> RemovedSerials { get; private set; }
private DisasmProject mProject;
private Formatter mFormatter;
private VisualizationSet mOrigSet;
private int mOffset;
/// <summary>
/// ItemsSource for visualizationGrid.
/// </summary>
public ObservableCollection<Visualization> VisualizationList { get; private set; } =
new ObservableCollection<Visualization>();
@ -99,6 +112,8 @@ namespace SourceGen.WpfGui {
mOrigSet = curSet;
mOffset = offset;
RemovedSerials = new List<int>();
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<VisualizationAnimation> needsUpdate = new List<VisualizationAnimation>();
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<int>(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) {