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":{ +}}