diff --git a/CommonWPF/AnimatedGifEncoder.cs b/CommonWPF/AnimatedGifEncoder.cs
index 4618dfa..40cee6f 100644
--- a/CommonWPF/AnimatedGifEncoder.cs
+++ b/CommonWPF/AnimatedGifEncoder.cs
@@ -122,14 +122,16 @@ namespace CommonWPF {
}
//
- // Step 2: determine the size of the largest image. This will become the logical
- // size of the animated GIF.
+ // Step 2: determine the dimensions of the largest image. This will become the
+ // logical size of the animated GIF.
//
// TODO(maybe): We have an opportunity to replace all of the local color tables with a
// single global color table. This is only possible if all of the local tables are
// identical and the transparency values in the GCE also match up. (Well, it's
// otherwise *possible*, but we'd need to decode, update palettes and pixels, and
// re-encode.)
+ // TODO(maybe): add an arg to Save() that causes it to use the first bitmap's
+ // palette as the global palette.
//
foreach (UnpackedGif gif in gifs) {
//gif.DebugDump();
diff --git a/PluginCommon/Interfaces.cs b/PluginCommon/Interfaces.cs
index bbc89c0..9f8b6ac 100644
--- a/PluginCommon/Interfaces.cs
+++ b/PluginCommon/Interfaces.cs
@@ -149,7 +149,7 @@ namespace PluginCommon {
///
/// Executes the specified visualization generator with the supplied parameters.
///
- /// VisGen identifier.
+ /// Visualization generator descriptor.
/// Parameter set.
/// 2D visualization object reference, or null if something went
/// wrong (unknown ident, bad parameters, etc). By convention, an error
@@ -157,6 +157,22 @@ namespace PluginCommon {
IVisualization2d Generate2d(VisDescr descr, ReadOnlyDictionary parms);
}
+ ///
+ /// Version 2 of the visualizer interface. Adds wireframe objects.
+ ///
+ public interface IPlugin_Visualizer_v2 : IPlugin_Visualizer {
+ ///
+ /// Executes the specified visualization generator with the supplied parameters.
+ ///
+ /// Visualization generator descriptor.
+ /// Parameter set.
+ /// Wireframe visualization object reference, or null if something went
+ /// wrong (unknown ident, bad parameters, etc). By convention, an error
+ /// message is reported through the IApplication ReportError interface.
+ IVisualizationWireframe GenerateWireframe(VisDescr descr,
+ ReadOnlyDictionary parms);
+ }
+
///
/// Visualization generator descriptor. IPlugin_Visualizer instances publish a list of
/// these to tell the application about the generators it supports.
@@ -178,6 +194,7 @@ namespace PluginCommon {
public enum VisType {
Unknown = 0,
Bitmap, // 2D bitmap
+ Wireframe, // wireframe mesh
}
///
@@ -302,12 +319,59 @@ namespace PluginCommon {
/// direct-color images.
/// Do not modify.
///
- ///
int[] GetPalette();
// TODO(maybe): report pixel aspect ratio?
}
+ ///
+ /// Holds raw vertex/edge/normal data collected from a 2D or 3D wireframe mesh. We use
+ /// a typical right-handed coordinate system (Z comes out of the screen), with the
+ /// expectation that the mesh will be presented such that the object is right-side up
+ /// and facing toward the viewer (nose toward +Z).
+ ///
+ /// All objects will have vertices and edges. Face normals are optional.
+ ///
+ ///
+ /// The face-normal stuff is designed specifically for Elite, which is one of the very
+ /// few games to use backface removal.
+ ///
+ /// We favor multiple arrays over compound objects for this interface to avoid having
+ /// to define those at the plugin interface level.
+ ///
+ /// TODO(maybe): specify colors for edges. Not widely used?
+ ///
+ public interface IVisualizationWireframe {
+ // Each function returns the specified data. Do not modify.
+
+ float[] GetVerticesX();
+ float[] GetVerticesY();
+ float[] GetVerticesZ();
+
+ IntPair[] GetEdges();
+
+ float[] GetNormalsX();
+ float[] GetNormalsY();
+ float[] GetNormalsZ();
+
+ IntPair[] GetVertexFaces();
+ IntPair[] GetEdgeFaces();
+ }
+
+ ///
+ /// Represents a pair of integers. Immutable.
+ ///
+ [Serializable]
+ public struct IntPair {
+ public int Val0 { get; private set; }
+ public int Val1 { get; private set; }
+
+ public IntPair(int val0, int val1) {
+ Val0 = val0;
+ Val1 = val1;
+ }
+ }
+
///
/// Interfaces provided by the application for use by plugins. An IApplication instance
/// is passed to the plugin as an argument to Prepare().
diff --git a/PluginCommon/VisWireframe.cs b/PluginCommon/VisWireframe.cs
new file mode 100644
index 0000000..cebc7b9
--- /dev/null
+++ b/PluginCommon/VisWireframe.cs
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2020 faddenSoft
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace PluginCommon {
+ ///
+ /// Wireframe mesh with optional backface normals, for use with visualization generators.
+ ///
+ [Serializable]
+ public class VisWireframe : IVisualizationWireframe {
+ private List mVerticesX = new List();
+ private List mVerticesY = new List();
+ private List mVerticesZ = new List();
+
+ private List mEdges = new List();
+
+ private List mNormalsX = new List();
+ private List mNormalsY = new List();
+ private List mNormalsZ = new List();
+
+ private List mVertexFaces = new List();
+ private List mEdgeFaces = new List();
+
+ ///
+ /// Constructor. Nothing much to do.
+ ///
+ public VisWireframe() { }
+
+ ///
+ /// Adds the vertex to the list.
+ ///
+ /// X coordinate.
+ /// Y coordinate.
+ /// Z coordinate.
+ /// Vertex index. Indices start at zero and count up.
+ public int AddVertex(float x, float y, float z) {
+ mVerticesX.Add(x);
+ mVerticesY.Add(y);
+ mVerticesZ.Add(z);
+ return mVerticesX.Count - 1;
+ }
+
+ ///
+ /// Adds the edge to the list.
+ ///
+ /// Index of first vertex.
+ /// Index of second vertex.
+ /// Edge index. Indices start at zero and count up.
+ public int AddEdge(int index0, int index1) {
+ Debug.Assert(index0 >= 0 && index0 < mVerticesX.Count);
+ Debug.Assert(index1 >= 0 && index1 < mVerticesX.Count);
+ mEdges.Add(new IntPair(index0, index1));
+ return mEdges.Count - 1;
+ }
+
+ ///
+ /// Adds the face normal to the list.
+ ///
+ /// X coordinate.
+ /// Y coordinate.
+ /// Z coordinate.
+ /// Face index. Indices start at zero and count up.
+ public int AddFaceNormal(float x, float y, float z) {
+ Debug.Assert(x != 0.0f || y != 0.0f || z != 0.0f); // no zero-length normals
+ mNormalsX.Add(x);
+ mNormalsY.Add(y);
+ mNormalsZ.Add(z);
+ return mNormalsX.Count - 1;
+ }
+
+ ///
+ /// Marks a vertex's visibility as being tied to the specified face. The face does
+ /// not need to be in the list yet.
+ ///
+ /// Index of vertex.
+ /// Index of face.
+ public void AddVertexFace(int vertexIndex, int faceIndex) {
+ Debug.Assert(vertexIndex >= 0 && vertexIndex < mVerticesX.Count);
+ mVertexFaces.Add(new IntPair(vertexIndex, faceIndex));
+ }
+
+ ///
+ /// Marks an edge's visibility as being tied to the specified face. The face does
+ /// not need to be in the list yet.
+ ///
+ /// Index of edge.
+ /// Index of face.
+ public void AddEdgeFace(int edgeIndex, int faceIndex) {
+ Debug.Assert(edgeIndex >= 0 && edgeIndex < mEdges.Count);
+ mEdgeFaces.Add(new IntPair(edgeIndex, faceIndex));
+ }
+
+ ///
+ /// Verifies that the various references by index are valid.
+ ///
+ /// Failure detail.
+ /// True if everything looks valid.
+ public bool Validate(out string msg) {
+ int vertexCount = mVerticesX.Count;
+ int faceCount = mNormalsX.Count;
+ int edgeCount = mEdges.Count;
+
+ // check edges
+ foreach (IntPair ip in mEdges) {
+ if (ip.Val0 < 0 || ip.Val0 >= vertexCount ||
+ ip.Val1 < 0 || ip.Val1 >= vertexCount) {
+ msg = "invalid edge";
+ return false;
+ }
+ }
+
+ // check vertex-faces
+ foreach (IntPair ip in mVertexFaces) {
+ if (ip.Val0 < 0 || ip.Val0 >= vertexCount ||
+ ip.Val1 < 0 || ip.Val1 >= faceCount) {
+ msg = "invalid vertex-face";
+ return false;
+ }
+ }
+
+ // check edge-faces
+ foreach (IntPair ip in mVertexFaces) {
+ if (ip.Val0 < 0 || ip.Val0 >= edgeCount ||
+ ip.Val1 < 0 || ip.Val1 >= faceCount) {
+ msg = "invalid edge-face";
+ return false;
+ }
+ }
+
+ // check face normals
+ for (int i = 0; i < mNormalsX.Count; i++) {
+ if (mNormalsX[i] == 0.0f && mNormalsY[i] == 0.0f && mNormalsZ[i] == 0.0f) {
+ msg = "zero-length normal";
+ return false;
+ }
+ }
+
+ msg = string.Empty;
+ return true;
+ }
+
+ //
+ // IVisualizationWireframe implementation.
+ //
+
+ public float[] GetVerticesX() {
+ return mVerticesX.ToArray();
+ }
+
+ public float[] GetVerticesY() {
+ return mVerticesY.ToArray();
+ }
+
+ public float[] GetVerticesZ() {
+ return mVerticesZ.ToArray();
+ }
+
+ public IntPair[] GetEdges() {
+ return mEdges.ToArray();
+ }
+
+ public float[] GetNormalsX() {
+ return mNormalsX.ToArray();
+ }
+
+ public float[] GetNormalsY() {
+ return mNormalsY.ToArray();
+ }
+
+ public float[] GetNormalsZ() {
+ return mNormalsZ.ToArray();
+ }
+
+ public IntPair[] GetVertexFaces() {
+ return mVertexFaces.ToArray();
+ }
+
+ public IntPair[] GetEdgeFaces() {
+ return mEdgeFaces.ToArray();
+ }
+
+
+ public override string ToString() {
+ return "[VisWireframe: " + mVerticesX.Count + " vertices, " +
+ mEdges.Count + " edges, " +
+ mNormalsX.Count + " faces, " +
+ mVertexFaces.Count + " vfaces, " +
+ mEdgeFaces.Count + " efaces]";
+ }
+ }
+}
diff --git a/SourceGen/SGTestData/Visualization/VisWireframeTest.cs b/SourceGen/SGTestData/Visualization/VisWireframeTest.cs
new file mode 100644
index 0000000..12cb916
--- /dev/null
+++ b/SourceGen/SGTestData/Visualization/VisWireframeTest.cs
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2020 faddenSoft
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+using System;
+using System.Collections.ObjectModel;
+
+using PluginCommon;
+
+namespace WireframeTest {
+ ///
+ /// Visualizer for wireframe test data.
+ ///
+ public class VisWireframeTest : MarshalByRefObject, IPlugin, IPlugin_Visualizer_v2 {
+ // IPlugin
+ public string Identifier {
+ get { return "Wireframe Test Data Visualizer"; }
+ }
+ private IApplication mAppRef;
+ private byte[] mFileData;
+
+ // Visualization identifiers; DO NOT change or projects that use them will break.
+ private const string VIS_GEN_WF_TEST = "wireframe-test";
+
+ private const string P_OFFSET = "offset";
+
+ // Visualization descriptors.
+ private VisDescr[] mDescriptors = new VisDescr[] {
+ new VisDescr(VIS_GEN_WF_TEST, "Wireframe Test Data", VisDescr.VisType.Wireframe,
+ new VisParamDescr[] {
+ new VisParamDescr("File offset (hex)",
+ P_OFFSET, typeof(int), 0, 0x00ffffff, VisParamDescr.SpecialMode.Offset, 0),
+ }),
+ };
+
+
+ // IPlugin
+ public void Prepare(IApplication appRef, byte[] fileData, AddressTranslate addrTrans) {
+ mAppRef = appRef;
+ mFileData = fileData;
+ }
+
+ // IPlugin
+ public void Unprepare() {
+ mAppRef = null;
+ mFileData = null;
+ }
+
+ // IPlugin_Visualizer
+ public VisDescr[] GetVisGenDescrs() {
+ if (mFileData == null) {
+ return null;
+ }
+ return mDescriptors;
+ }
+
+ // IPlugin_Visualizer
+ public IVisualization2d Generate2d(VisDescr descr,
+ ReadOnlyDictionary parms) {
+ mAppRef.ReportError("2d not supported");
+ return null;
+ }
+
+ // IPlugin_Visualizer_v2
+ public IVisualizationWireframe GenerateWireframe(VisDescr descr,
+ ReadOnlyDictionary parms) {
+ switch (descr.Ident) {
+ case VIS_GEN_WF_TEST:
+ return GenerateWireframe(parms);
+ default:
+ mAppRef.ReportError("Unknown ident " + descr.Ident);
+ return null;
+ }
+ }
+
+ private IVisualizationWireframe GenerateWireframe(ReadOnlyDictionary parms) {
+ int offset = Util.GetFromObjDict(parms, P_OFFSET, 0);
+
+ if (offset < 0 || offset >= mFileData.Length) {
+ // should be caught by editor
+ mAppRef.ReportError("Invalid parameter");
+ return null;
+ }
+
+ VisWireframe vw = new VisWireframe();
+ const sbyte END_MARKER = -128; // 0x80
+ try {
+ while (true) {
+ int vx = (sbyte)mFileData[offset++];
+ if (vx == END_MARKER) {
+ break;
+ }
+ int vy = (sbyte)mFileData[offset++];
+ int vz = (sbyte)mFileData[offset++];
+
+ vw.AddVertex(vx, vy, vz);
+ }
+
+ while (true) {
+ int v0 = (sbyte)mFileData[offset++];
+ if (v0 == END_MARKER) {
+ break;
+ }
+ int v1 = mFileData[offset++];
+ int f0 = mFileData[offset++];
+ int f1 = mFileData[offset++];
+
+ int edge = vw.AddEdge(v0, v1);
+ vw.AddEdgeFace(edge, f0);
+ vw.AddEdgeFace(edge, f1);
+ }
+
+ while (true) {
+ int nx = (sbyte)mFileData[offset++];
+ if (nx == END_MARKER) {
+ break;
+ }
+ int ny = (sbyte)mFileData[offset++];
+ int nz = (sbyte)mFileData[offset++];
+
+ vw.AddFaceNormal(nx, ny, nz);
+ }
+ } catch (IndexOutOfRangeException) {
+ // assume it was our file data access that caused the failure
+ mAppRef.ReportError("Ran off end of file");
+ return null;
+ }
+
+ string msg;
+ if (!vw.Validate(out msg)) {
+ mAppRef.ReportError("Data error: " + msg);
+ return null;
+ }
+
+ return vw;
+ }
+ }
+}
diff --git a/SourceGen/SGTestData/Visualization/wireframe-test b/SourceGen/SGTestData/Visualization/wireframe-test
new file mode 100644
index 0000000..1804db8
Binary files /dev/null and b/SourceGen/SGTestData/Visualization/wireframe-test differ
diff --git a/SourceGen/SGTestData/Visualization/wireframe-test.S b/SourceGen/SGTestData/Visualization/wireframe-test.S
new file mode 100644
index 0000000..87d94c2
--- /dev/null
+++ b/SourceGen/SGTestData/Visualization/wireframe-test.S
@@ -0,0 +1,58 @@
+; Copyright 2020 faddenSoft. All Rights Reserved.
+; See the LICENSE.txt file for distribution terms (Apache 2.0).
+;
+; Assembler: Merlin 32
+
+ org $1000
+
+ lda vertices
+ lda edges
+ lda faces
+ rts
+
+; List of vertices (X,Y,Z).
+vertices
+ dfb -32,32,32 ;0
+ dfb -32,-32,32 ;1
+ dfb 32,-32,32 ;2
+ dfb 32,32,32 ;3
+ dfb -32,32,-32 ;4
+ dfb -32,-32,-32 ;5
+ dfb 32,-32,-32 ;6
+ dfb 32,32,-32 ;7
+
+; put a decoration on the front face; should look like a '7'
+ dfb -20,-20,32 ;8
+ dfb 20,20,32 ;9
+ dfb 10,20,32 ;10
+ dfb $80
+
+; List of edges (vertex0, vertex1, face0, face1).
+edges
+ dfb 0,1, 0,1 ;0
+ dfb 1,2, 0,3 ;1
+ dfb 2,3, 0,4 ;2
+ dfb 3,0, 0,2 ;3
+ dfb 4,5, 1,5 ;4
+ dfb 5,6, 1,3 ;5
+ dfb 6,7, 1,4 ;6
+ dfb 7,4, 1,2 ;7
+ dfb 0,4, 2,5 ;8
+ dfb 1,5, 3,5 ;9
+ dfb 2,6, 3,4 ;10
+ dfb 3,7, 2,4 ;11
+
+ dfb 8,9, 0,0 ;12
+ dfb 9,10,0,0 ;13
+ dfb $80
+
+; List of faces (surface normal X,Y,Z).
+faces
+ dfb 0,0,1 ;0 front
+ dfb 0,0,-1 ;1 back
+ dfb 0,1,0 ;2 top
+ dfb 0,-1,0 ;3 bottom
+ dfb 1,0,0 ;4 right
+ dfb -1,0,0 ;5 left
+ dfb $80
+
diff --git a/SourceGen/SGTestData/Visualization/wireframe-test.dis65 b/SourceGen/SGTestData/Visualization/wireframe-test.dis65
new file mode 100644
index 0000000..56c868a
--- /dev/null
+++ b/SourceGen/SGTestData/Visualization/wireframe-test.dis65
@@ -0,0 +1,245 @@
+### 6502bench SourceGen dis65 v1.0 ###
+{
+"_ContentVersion":3,
+"FileDataLength":120,
+"FileDataCrc32":1432202158,
+"ProjectProps":{
+"CpuName":"6502",
+"IncludeUndocumentedInstr":false,
+"TwoByteBrk":false,
+"EntryFlags":32702671,
+"AutoLabelStyle":"Simple",
+"AnalysisParams":{
+"AnalyzeUncategorizedData":true,
+"DefaultTextScanMode":"LowHighAscii",
+"MinCharsForString":4,
+"SeekNearbyTargets":true,
+"SmartPlpHandling":true},
+
+"PlatformSymbolFileIdentifiers":[],
+"ExtensionScriptFileIdentifiers":["PROJ:VisWireframeTest.cs"],
+"ProjectSyms":{
+}},
+
+"AddressMap":[{
+"Offset":0,
+"Addr":4096}],
+"TypeHints":[{
+"Low":0,
+"High":0,
+"Hint":"Code"}],
+"StatusFlagOverrides":{
+},
+
+"Comments":{
+},
+
+"LongComments":{
+},
+
+"Notes":{
+},
+
+"UserLabels":{
+},
+
+"OperandFormats":{
+"10":{
+"Length":3,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"13":{
+"Length":3,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"16":{
+"Length":3,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"19":{
+"Length":3,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"22":{
+"Length":3,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"25":{
+"Length":3,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"28":{
+"Length":3,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"31":{
+"Length":3,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"34":{
+"Length":3,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"37":{
+"Length":3,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"40":{
+"Length":3,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"43":{
+"Length":1,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"44":{
+"Length":4,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"48":{
+"Length":4,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"52":{
+"Length":4,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"56":{
+"Length":4,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"60":{
+"Length":4,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"64":{
+"Length":4,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"68":{
+"Length":4,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"72":{
+"Length":4,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"76":{
+"Length":4,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"80":{
+"Length":4,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"84":{
+"Length":4,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"88":{
+"Length":4,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"92":{
+"Length":4,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"96":{
+"Length":4,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"101":{
+"Length":3,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"104":{
+"Length":3,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"107":{
+"Length":3,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"110":{
+"Length":3,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"113":{
+"Length":3,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null},
+
+"116":{
+"Length":3,
+"Format":"Dense",
+"SubFormat":"None",
+"SymbolRef":null}},
+
+"LvTables":{
+},
+
+"Visualizations":[],
+"VisualizationAnimations":[],
+"VisualizationSets":{
+}}