2
0
Эх сурвалжийг харах

WIP: Refactoring loader API

Vicente Penades 6 жил өмнө
parent
commit
a776c2e8cc

+ 1 - 1
src/SharpGLTF.Core/Memory/MemoryAccessor.cs

@@ -198,7 +198,7 @@ namespace SharpGLTF.Memory
 
         #endregion
 
-        #region types
+        #region nested types
 
         private static int _GetSortingScore(string attribute)
         {

+ 5 - 5
src/SharpGLTF.Core/Schema2/gltf.Accessors.cs

@@ -456,7 +456,7 @@ namespace SharpGLTF.Schema2
 
             for (int i = 0; i < normals.Count; ++i)
             {
-                if (result.TryFixUnitLength(i, normals[i]))
+                if (result.TryFixUnitLengthOrError(i, normals[i]))
                 {
                     normals[i] = normals[i].SanitizeNormal();
                 }
@@ -476,7 +476,7 @@ namespace SharpGLTF.Schema2
 
             for (int i = 0; i < tangents.Count; ++i)
             {
-                if (result.TryFixTangent(i, tangents[i]))
+                if (result.TryFixTangentOrError(i, tangents[i]))
                 {
                     tangents[i] = tangents[i].SanitizeTangent();
                 }
@@ -523,7 +523,7 @@ namespace SharpGLTF.Schema2
         {
             result = result.GetContext(this);
 
-            SourceBufferView.ValidateBufferUsageData(result);
+            SourceBufferView.ValidateBufferUsagePlainData(result);
             result.CheckLinkMustBeAnyOf(nameof(Dimensions), Dimensions, DimensionType.MAT4);
 
             var matrices = this.AsMatrix4x4Array();
@@ -536,13 +536,13 @@ namespace SharpGLTF.Schema2
 
         internal void ValidateAnimationInput(Validation.ValidationContext result)
         {
-            SourceBufferView.ValidateBufferUsageData(result);
+            SourceBufferView.ValidateBufferUsagePlainData(result);
             result.CheckLinkMustBeAnyOf(nameof(Dimensions), Dimensions, DimensionType.SCALAR);
         }
 
         internal void ValidateAnimationOutput(Validation.ValidationContext result)
         {
-            SourceBufferView.ValidateBufferUsageData(result);
+            SourceBufferView.ValidateBufferUsagePlainData(result);
             result.CheckLinkMustBeAnyOf(nameof(Dimensions), Dimensions, DimensionType.SCALAR, DimensionType.VEC3, DimensionType.VEC4);
         }
 

+ 4 - 4
src/SharpGLTF.Core/Schema2/gltf.Buffer.cs

@@ -44,18 +44,18 @@ namespace SharpGLTF.Schema2
         const string EMBEDDEDOCTETSTREAM = "data:application/octet-stream;base64,";
         const string EMBEDDEDGLTFBUFFER = "data:application/gltf-buffer;base64,";
 
-        internal void _ResolveUri(AssetReader satelliteReferenceSolver)
+        internal void _ResolveUri(ReadContext context)
         {
-            _Content = _LoadBinaryBufferUnchecked(_uri, satelliteReferenceSolver);
+            _Content = _LoadBinaryBufferUnchecked(_uri, context);
 
             _uri = null; // When _Data is not empty, clear URI
         }
 
-        private static Byte[] _LoadBinaryBufferUnchecked(string uri, AssetReader satelliteReferenceSolver)
+        private static Byte[] _LoadBinaryBufferUnchecked(string uri, ReadContext context)
         {
             return uri._TryParseBase64Unchecked(EMBEDDEDGLTFBUFFER)
                 ?? uri._TryParseBase64Unchecked(EMBEDDEDOCTETSTREAM)
-                ?? satelliteReferenceSolver.Invoke(uri).ToArray();
+                ?? context.ReadBytes(uri).ToArray();
         }
 
         #endregion

+ 5 - 3
src/SharpGLTF.Core/Schema2/gltf.BufferView.cs

@@ -201,6 +201,8 @@ namespace SharpGLTF.Schema2
             if (bv.IsIndexBuffer)
             {
                 if (dim != DimensionType.SCALAR) result.AddLinkError(("BufferView", bv.LogicalIndex), $"is an IndexBuffer, but accessor dimensions is: {dim}");
+
+                // TODO: these could by fixed by replacing BYTE by UBYTE, SHORT by USHORT, etc
                 if (enc == EncodingType.BYTE)    result.AddLinkError(("BufferView", bv.LogicalIndex), $"is an IndexBuffer, but accessor encoding is (s)byte");
                 if (enc == EncodingType.SHORT)   result.AddLinkError(("BufferView", bv.LogicalIndex), $"is an IndexBuffer, but accessor encoding is (s)short");
                 if (enc == EncodingType.FLOAT)   result.AddLinkError(("BufferView", bv.LogicalIndex), $"is an IndexBuffer, but accessor encoding is float");
@@ -264,11 +266,11 @@ namespace SharpGLTF.Schema2
             result.AddLinkError("Device Buffer Target", $"is set as {this._target.Value}. But an accessor wants to use it as '{usingMode}'.");
         }
 
-        internal void ValidateBufferUsageData(Validation.ValidationContext result)
+        internal void ValidateBufferUsagePlainData(Validation.ValidationContext result)
         {
             if (this._byteStride.HasValue)
             {
-                if (result.TryFixLink("BufferView", "Unexpected ByteStride found. Expected null"))
+                if (result.TryFixLinkOrError("BufferView", "Unexpected ByteStride found. Expected null"))
                 {
                     this._byteStride = null;
                 }
@@ -278,7 +280,7 @@ namespace SharpGLTF.Schema2
 
             if (!this._target.HasValue) return;
 
-            if (result.TryFixLink("Device Buffer Target", $"is set as {this._target.Value}. But an accessor wants to use it as a plain data buffer."))
+            if (result.TryFixLinkOrError("Device Buffer Target", $"is set as {this._target.Value}. But an accessor wants to use it as a plain data buffer."))
             {
                 this._target = null;
             }

+ 26 - 7
src/SharpGLTF.Core/Schema2/gltf.Images.cs

@@ -101,6 +101,16 @@ namespace SharpGLTF.Schema2
 
         internal int _SourceBufferViewIndex => _bufferView.AsValue(-1);
 
+        internal bool _HasContent
+        {
+            get
+            {
+                if (_bufferView != null) return true;
+                if (_SatelliteImageContent != null) return true;
+                return false;
+            }
+        }
+
         #endregion
 
         #region API
@@ -168,9 +178,8 @@ namespace SharpGLTF.Schema2
 
             if (imageType == null) throw new ArgumentException($"{nameof(content)} must be a PNG, JPG, DDS or WEBP image", nameof(content));
 
-            this._uri = null;
-            this._mimeType = null;
-            this._bufferView = null;
+            _DiscardContent();
+
             this._SatelliteImageContent = content;
         }
 
@@ -196,24 +205,34 @@ namespace SharpGLTF.Schema2
 
         #region binary read
 
-        internal void _ResolveUri(AssetReader externalReferenceSolver)
+        internal void _ResolveUri(ReadContext context)
         {
             if (!String.IsNullOrWhiteSpace(_uri))
             {
-                _SatelliteImageContent = _LoadImageUnchecked(externalReferenceSolver, _uri);
+                var data = _LoadImageUnchecked(context, _uri);
+
+                _SatelliteImageContent = data;
                 _uri = null;
                 _mimeType = null;
             }
         }
 
-        private static Byte[] _LoadImageUnchecked(AssetReader externalReferenceSolver, string uri)
+        private static Byte[] _LoadImageUnchecked(ReadContext context, string uri)
         {
             return uri._TryParseBase64Unchecked(EMBEDDED_GLTF_BUFFER)
                 ?? uri._TryParseBase64Unchecked(EMBEDDED_OCTET_STREAM)
                 ?? uri._TryParseBase64Unchecked(EMBEDDED_JPEG_BUFFER)
                 ?? uri._TryParseBase64Unchecked(EMBEDDED_PNG_BUFFER)
                 ?? uri._TryParseBase64Unchecked(EMBEDDED_DDS_BUFFER)
-                ?? externalReferenceSolver.Invoke(uri).ToArray();
+                ?? context.ReadBytes(uri).ToArray();
+        }
+
+        internal void _DiscardContent()
+        {
+            this._uri = null;
+            this._mimeType = null;
+            this._bufferView = null;
+            this._SatelliteImageContent = null;
         }
 
         #endregion

+ 4 - 2
src/SharpGLTF.Core/Schema2/gltf.Root.cs

@@ -54,13 +54,15 @@ namespace SharpGLTF.Schema2
         public ModelRoot DeepClone()
         {
             var dict = new Dictionary<string, ArraySegment<Byte>>();
-            var settings = WriteSettings.ForDeepClone(dict);
+            var settings = WriteContext.ForDeepClone(dict);
 
             System.Diagnostics.Debug.Assert(settings._NoCloneWatchdog, "invalid clone settings");
 
             this.Write(settings, "deepclone");
 
-            return ModelRoot.ReadFromDictionary(dict, "deepclone.gltf");
+            var context = ReadContext.CreateFromDictionary(dict);
+            context.Validation = Validation.ValidationMode.Strict;
+            return context._ReadFromDictionary("deepclone.gltf");
         }
 
         #endregion

+ 192 - 111
src/SharpGLTF.Core/Schema2/gltf.Serialization.Read.cs

@@ -18,134 +18,138 @@ namespace SharpGLTF.Schema2
     /// </summary>
     /// <param name="assetName">the asset relative path.</param>
     /// <returns>The file contents as a <see cref="byte"/> array.</returns>
-    public delegate BYTES AssetReader(String assetName);
+    public delegate BYTES FileReaderCallback(String assetName);
+
+    public delegate Boolean ImageReaderCallback(Image image);
 
     /// <summary>
     /// Configuration settings for reading model files.
     /// </summary>
     public class ReadSettings
     {
-        public ReadSettings()
-        {
-        }
+        #region lifecycle
 
-        internal ReadSettings(AssetReader reader)
-        {
-            FileReader = reader;
-        }
+        public ReadSettings() { }
 
-        internal ReadSettings(string filePath)
+        public ReadSettings(ReadSettings other)
         {
-            Guard.FilePathMustExist(filePath, nameof(filePath));
-
-            var dir = Path.GetDirectoryName(filePath);
-
-            FileReader = assetFileName => new BYTES(File.ReadAllBytes(Path.Combine(dir, assetFileName)));
+            this.Validation = other.Validation;
         }
 
-        /// <summary>
-        /// Gets or sets the <see cref="AssetReader"/> delegate used to read satellite files.
-        /// </summary>
-        public AssetReader FileReader { get; set; }
+        #endregion
+
+        #region properties
 
         /// <summary>
         /// Gets or sets a value indicating the level of validation applied when loading a file.
         /// </summary>
         public VALIDATIONMODE Validation { get; set; } = VALIDATIONMODE.Strict;
+
+        #endregion
     }
 
-    partial class ModelRoot
+    public class ReadContext : ReadSettings
     {
-        #region validate
+        #region lifecycle
 
-        public static Validation.ValidationResult Validate(string filePath)
+        public static ReadContext Create(FileReaderCallback callback)
+        {
+            Guard.NotNull(callback, nameof(callback));
+
+            return new ReadContext(callback);
+        }
+
+        public static ReadContext CreateFromFile(string filePath)
         {
             Guard.FilePathMustExist(filePath, nameof(filePath));
 
-            var settings = new ReadSettings(filePath);
+            var dir = Path.GetDirectoryName(filePath);
 
-            using (var stream = File.OpenRead(filePath))
-            {
-                bool binaryFile = glb._Identify(stream);
+            return CreateFromDirectory(dir);
+        }
 
-                if (binaryFile) return _ReadGLB(stream, settings).Validation;
+        public static ReadContext CreateFromDirectory(string directoryPath)
+        {
+            return new ReadContext(assetFileName => new BYTES(File.ReadAllBytes(Path.Combine(directoryPath, assetFileName))));
+        }
 
-                string content = null;
+        public static ReadContext CreateFromDictionary(IReadOnlyDictionary<string, BYTES> dictionary)
+        {
+            return new ReadContext(fn => dictionary[fn]);
+        }
 
-                using (var streamReader = new StreamReader(stream))
-                {
-                    content = streamReader.ReadToEnd();
-                }
+        private ReadContext(FileReaderCallback reader)
+        {
+            _FileReader = reader;
+        }
 
-                return _ParseGLTF(content, settings).Validation;
-            }
+        internal ReadContext(ReadContext other)
+            : base(other)
+        {
+            this._FileReader = other._FileReader;
+            this.ImageReader = other.ImageReader;
         }
 
         #endregion
 
-        #region read / load methods
+        #region data
+
+        private FileReaderCallback _FileReader;
 
         /// <summary>
-        /// Reads a <see cref="MODEL"/> instance from a path pointing to a GLB or a GLTF file
+        /// When loading GLB, this represents the internal binary data chunk.
         /// </summary>
-        /// <param name="filePath">A valid file path.</param>
-        /// <param name="vmode">Defines the file validation level.</param>
-        /// <returns>A <see cref="MODEL"/> instance.</returns>
-        public static MODEL Load(string filePath, VALIDATIONMODE vmode = VALIDATIONMODE.Strict)
-        {
-            Guard.FilePathMustExist(filePath, nameof(filePath));
+        private Byte[] _BinaryChunk;
 
-            var settings = new ReadSettings(filePath);
+        #endregion
 
-            settings.Validation = vmode;
+        #region callbacks
 
-            using (var s = File.OpenRead(filePath))
+        public ImageReaderCallback ImageReader { get; set; }
+
+        #endregion
+
+        #region API
+
+        public BYTES ReadBytes(string fileName)
+        {
+            if (_BinaryChunk != null)
             {
-                return Read(s, settings);
+                if (string.IsNullOrEmpty(fileName)) return new BYTES(_BinaryChunk);
             }
+
+            return _FileReader(fileName);
         }
 
-        /// <summary>
-        /// Parses a <see cref="MODEL"/> instance from a <see cref="byte"/> array representing a GLB file
-        /// </summary>
-        /// <param name="glb">A <see cref="byte"/> array representing a GLB file</param>
-        /// <returns>A <see cref="MODEL"/> instance.</returns>
-        public static MODEL ParseGLB(BYTES glb)
+        public Stream OpenFile(string fileName)
         {
-            Guard.NotNull(glb, nameof(glb));
+            var content = _FileReader(fileName);
 
-            using (var m = new MemoryStream(glb.Array, glb.Offset, glb.Count, false))
-            {
-                return ReadGLB(m, new ReadSettings());
-            }
+            return new MemoryStream(content.Array, content.Offset, content.Count);
         }
 
         /// <summary>
         /// Reads a <see cref="MODEL"/> instance from a <see cref="Stream"/> containing a GLB or a GLTF file.
         /// </summary>
         /// <param name="stream">A <see cref="Stream"/> to read from.</param>
-        /// <param name="settings">A <see cref="ReadSettings"/> instance defining the reading options.</param>
         /// <returns>A <see cref="MODEL"/> instance.</returns>
-        public static MODEL Read(Stream stream, ReadSettings settings)
+        public MODEL Read(Stream stream)
         {
             Guard.NotNull(stream, nameof(stream));
 
             bool binaryFile = glb._Identify(stream);
 
-            if (binaryFile) return ReadGLB(stream, settings);
-            else            return ReadGLTF(stream, settings);
+            return binaryFile ? ReadGLB(stream) : ReadGLTF(stream);
         }
 
         /// <summary>
         /// Reads a <see cref="MODEL"/> instance from a <see cref="Stream"/> containing a GLTF file.
         /// </summary>
         /// <param name="stream">A <see cref="Stream"/> to read from.</param>
-        /// <param name="settings">A <see cref="ReadSettings"/> instance defining the reading options.</param>
         /// <returns>A <see cref="MODEL"/> instance.</returns>
-        public static MODEL ReadGLTF(Stream stream, ReadSettings settings)
+        public MODEL ReadGLTF(Stream stream)
         {
             Guard.NotNull(stream, nameof(stream));
-            Guard.NotNull(settings, nameof(settings));
 
             string content = null;
 
@@ -154,21 +158,19 @@ namespace SharpGLTF.Schema2
                 content = streamReader.ReadToEnd();
             }
 
-            return ParseGLTF(content, settings);
+            return ParseGLTF(content);
         }
 
         /// <summary>
         /// Reads a <see cref="MODEL"/> instance from a <see cref="Stream"/> containing a GLB file.
         /// </summary>
         /// <param name="stream">A <see cref="Stream"/> to read from.</param>
-        /// <param name="settings">A <see cref="ReadSettings"/> instance defining the reading options.</param>
         /// <returns>A <see cref="MODEL"/> instance.</returns>
-        public static MODEL ReadGLB(Stream stream, ReadSettings settings)
+        public MODEL ReadGLB(Stream stream)
         {
             Guard.NotNull(stream, nameof(stream));
-            Guard.NotNull(settings, nameof(settings));
 
-            var mv = _ReadGLB(stream, settings);
+            var mv = _ReadGLB(stream);
 
             if (mv.Validation.HasErrors) throw mv.Validation.Errors.FirstOrDefault();
 
@@ -179,32 +181,42 @@ namespace SharpGLTF.Schema2
         /// Parses a <see cref="MODEL"/> instance from a <see cref="String"/> JSON content representing a GLTF file.
         /// </summary>
         /// <param name="jsonContent">A <see cref="String"/> JSON content representing a GLTF file.</param>
-        /// <param name="settings">A <see cref="ReadSettings"/> instance defining the reading options.</param>
         /// <returns>A <see cref="MODEL"/> instance.</returns>
-        public static MODEL ParseGLTF(String jsonContent, ReadSettings settings)
+        public MODEL ParseGLTF(String jsonContent)
         {
-            var mv = _ParseGLTF(jsonContent, settings);
+            var mv = _ParseGLTF(jsonContent);
 
             if (mv.Validation.HasErrors) throw mv.Validation.Errors.FirstOrDefault();
 
             return mv.Model;
         }
 
-        public static MODEL ReadFromDictionary(Dictionary<string, BYTES> files, string fileName, VALIDATIONMODE vmode = VALIDATIONMODE.Strict)
+        public Validation.ValidationResult Validate(string filePath)
         {
-            Guard.NotNull(files, nameof(files));
+            using (var stream = File.OpenRead(filePath))
+            {
+                bool binaryFile = glb._Identify(stream);
 
-            var jsonBytes = files[fileName];
+                if (binaryFile) return _ReadGLB(stream).Validation;
 
-            var settings = new ReadSettings(fn => files[fn]);
+                string content = null;
+
+                using (var streamReader = new StreamReader(stream))
+                {
+                    content = streamReader.ReadToEnd();
+                }
 
-            settings.Validation = vmode;
+                return _ParseGLTF(content).Validation;
+            }
+        }
 
-            using (var m = new MemoryStream(jsonBytes.Array, jsonBytes.Offset, jsonBytes.Count))
+        internal MODEL _ReadFromDictionary(string fileName)
+        {
+            using (var s = this.OpenFile(fileName))
             {
-                using (var tr = new StreamReader(m))
+                using (var tr = new StreamReader(s))
                 {
-                    var mv = _Read(tr, settings);
+                    var mv = this._Read(tr);
 
                     if (mv.Validation.HasErrors) throw mv.Validation.Errors.FirstOrDefault();
 
@@ -215,51 +227,42 @@ namespace SharpGLTF.Schema2
 
         #endregion
 
-        #region reading core
+        #region core
 
-        private static (MODEL Model, Validation.ValidationResult Validation) _ReadGLB(Stream stream, ReadSettings settings)
+        private (MODEL Model, Validation.ValidationResult Validation) _ReadGLB(Stream stream)
         {
             Guard.NotNull(stream, nameof(stream));
-            Guard.NotNull(settings, nameof(settings));
 
             var chunks = glb.ReadBinaryFile(stream);
 
             var dom = Encoding.UTF8.GetString(chunks[glb.CHUNKJSON]);
 
+            var context = this;
+
             if (chunks.ContainsKey(glb.CHUNKBIN))
             {
-                var sourceReader = settings.FileReader;
-
-                settings.FileReader =
-                    key =>
-                    string.IsNullOrEmpty(key)
-                    ?
-                    new BYTES(chunks[glb.CHUNKBIN])
-                    :
-                    sourceReader.Invoke(key);
+                context = new ReadContext(context); // clone instance
+                context._BinaryChunk = chunks[glb.CHUNKBIN];
             }
 
-            return _ParseGLTF(dom, settings);
+            return context._ParseGLTF(dom);
         }
 
-        private static (MODEL Model, Validation.ValidationResult Validation) _ParseGLTF(String jsonContent, ReadSettings settings)
+        private (MODEL Model, Validation.ValidationResult Validation) _ParseGLTF(String jsonContent)
         {
             Guard.NotNullOrEmpty(jsonContent, nameof(jsonContent));
-            Guard.NotNull(settings, nameof(settings));
-
             using (var tr = new StringReader(jsonContent))
             {
-                return _Read(tr, settings);
+                return _Read(tr);
             }
         }
 
-        private static (MODEL Model, Validation.ValidationResult Validation) _Read(TextReader textReader, ReadSettings settings)
+        private (MODEL Model, Validation.ValidationResult Validation) _Read(TextReader textReader)
         {
             Guard.NotNull(textReader, nameof(textReader));
-            Guard.NotNull(settings, nameof(settings));
 
             var root = new MODEL();
-            var vcontext = new Validation.ValidationResult(root, settings.Validation);
+            var vcontext = new Validation.ValidationResult(root, this.Validation);
 
             using (var reader = new JsonTextReader(textReader))
             {
@@ -282,32 +285,110 @@ namespace SharpGLTF.Schema2
 
             // schema validation
 
-            root.ValidateReferences(vcontext.GetContext(root));
+            root.ValidateReferences(vcontext.GetContext());
             var ex = vcontext.Errors.FirstOrDefault();
             if (ex != null) return (null, vcontext);
 
-            // resolve external references
+            // resolve external dependencies
+
+            root._ResolveSatelliteDependencies(this);
 
-            foreach (var buffer in root._buffers)
+            // full validation
+
+            if (this.Validation != VALIDATIONMODE.Skip)
             {
-                buffer._ResolveUri(settings.FileReader);
+                root.Validate(vcontext.GetContext());
+                ex = vcontext.Errors.FirstOrDefault();
+                if (ex != null) return (null, vcontext);
             }
 
-            foreach (var image in root._images)
+            return (root, vcontext);
+        }
+
+        #endregion
+    }
+
+    partial class ModelRoot
+    {
+        #region validate
+
+        public static Validation.ValidationResult Validate(string filePath)
+        {
+            Guard.FilePathMustExist(filePath, nameof(filePath));
+
+            var context = ReadContext.CreateFromFile(filePath);
+
+            return context.Validate(filePath);
+        }
+
+        #endregion
+
+        #region read / load methods
+
+        /// <summary>
+        /// Reads a <see cref="MODEL"/> instance from a path pointing to a GLB or a GLTF file
+        /// </summary>
+        /// <param name="filePath">A valid file path.</param>
+        /// <param name="vmode">Defines the file validation level.</param>
+        /// <returns>A <see cref="MODEL"/> instance.</returns>
+        public static MODEL Load(string filePath, VALIDATIONMODE vmode = VALIDATIONMODE.Strict)
+        {
+            Guard.FilePathMustExist(filePath, nameof(filePath));
+
+            var context = ReadContext.CreateFromFile(filePath);
+
+            context.Validation = vmode;
+
+            using (var s = File.OpenRead(filePath))
             {
-                image._ResolveUri(settings.FileReader);
+                return context.Read(s);
             }
+        }
 
-            // full validation
+        /// <summary>
+        /// Parses a <see cref="MODEL"/> instance from a <see cref="byte"/> array representing a GLB file
+        /// </summary>
+        /// <param name="glb">A <see cref="byte"/> array representing a GLB file</param>
+        /// <returns>A <see cref="MODEL"/> instance.</returns>
+        public static MODEL ParseGLB(BYTES glb)
+        {
+            Guard.NotNull(glb, nameof(glb));
 
-            if (settings.Validation != VALIDATIONMODE.Skip)
+            var context = ReadContext.Create(f => throw new NotSupportedException());
+
+            using (var m = new MemoryStream(glb.Array, glb.Offset, glb.Count, false))
             {
-                root.Validate(vcontext.GetContext(root));
-                ex = vcontext.Errors.FirstOrDefault();
-                if (ex != null) return (null, vcontext);
+                return context.ReadGLB(m);
             }
+        }
 
-            return (root, vcontext);
+        #endregion
+
+        #region externals resolver
+
+        internal void _ResolveSatelliteDependencies(ReadContext context)
+        {
+            // resolve satellite buffers
+
+            foreach (var buffer in this._buffers)
+            {
+                buffer._ResolveUri(context);
+            }
+
+            // resolve satellite images
+
+            foreach (var image in this._images)
+            {
+                image._ResolveUri(context);
+
+                if (context.ImageReader != null)
+                {
+                    if (!context.ImageReader(image))
+                    {
+                        image._DiscardContent();
+                    }
+                }
+            }
         }
 
         #endregion

+ 32 - 23
src/SharpGLTF.Core/Schema2/gltf.Serialization.Write.cs

@@ -43,7 +43,7 @@ namespace SharpGLTF.Schema2
     /// <summary>
     /// Configuration settings for writing model files.
     /// </summary>
-    public class WriteSettings
+    public class WriteContext
     {
         #region lifecycle
 
@@ -51,10 +51,10 @@ namespace SharpGLTF.Schema2
         /// These settings are used exclusively by <see cref="MODEL.DeepClone"/>.
         /// </summary>
         /// <param name="dict">The dictionary where the model will be stored</param>
-        /// <returns>The settings to use with <see cref="MODEL.Write(WriteSettings, string)"/></returns>
-        internal static WriteSettings ForDeepClone(Dictionary<string, BYTES> dict)
+        /// <returns>The settings to use with <see cref="MODEL.Write(WriteContext, string)"/></returns>
+        internal static WriteContext ForDeepClone(Dictionary<string, BYTES> dict)
         {
-            var settings = new WriteSettings()
+            var settings = new WriteContext()
             {
                 BinaryMode = false,
                 ImageWriting = ResourceWriteMode.SatelliteFile,
@@ -69,13 +69,13 @@ namespace SharpGLTF.Schema2
             return settings;
         }
 
-        internal static WriteSettings ForText(string filePath)
+        internal static WriteContext ForText(string filePath)
         {
             Guard.FilePathMustBeValid(filePath, nameof(filePath));
 
             var dir = Path.GetDirectoryName(filePath);
 
-            var settings = new WriteSettings
+            var settings = new WriteContext
             {
                 BinaryMode = false,
                 ImageWriting = ResourceWriteMode.SatelliteFile,
@@ -89,9 +89,9 @@ namespace SharpGLTF.Schema2
             return settings;
         }
 
-        internal static WriteSettings ForText(Dictionary<string, BYTES> dict)
+        internal static WriteContext ForText(Dictionary<string, BYTES> dict)
         {
-            var settings = new WriteSettings()
+            var settings = new WriteContext()
             {
                 BinaryMode = false,
                 ImageWriting = ResourceWriteMode.SatelliteFile,
@@ -105,13 +105,13 @@ namespace SharpGLTF.Schema2
             return settings;
         }
 
-        internal static WriteSettings ForBinary(string filePath)
+        internal static WriteContext ForBinary(string filePath)
         {
             Guard.FilePathMustBeValid(filePath, nameof(filePath));
 
             var dir = Path.GetDirectoryName(filePath);
 
-            var settings = new WriteSettings
+            var settings = new WriteContext
             {
                 BinaryMode = true,
                 ImageWriting = ResourceWriteMode.BufferView,
@@ -125,12 +125,12 @@ namespace SharpGLTF.Schema2
             return settings;
         }
 
-        internal static WriteSettings ForBinary(Stream stream)
+        internal static WriteContext ForBinary(Stream stream)
         {
             Guard.NotNull(stream, nameof(stream));
             Guard.IsTrue(stream.CanWrite, nameof(stream));
 
-            var settings = new WriteSettings
+            var settings = new WriteContext
             {
                 BinaryMode = true,
                 ImageWriting = ResourceWriteMode.BufferView,
@@ -249,7 +249,7 @@ namespace SharpGLTF.Schema2
         {
             Guard.FilePathMustBeValid(filePath, nameof(filePath));
 
-            var settings = WriteSettings.ForBinary(filePath);
+            var settings = WriteContext.ForBinary(filePath);
 
             var name = Path.GetFileNameWithoutExtension(filePath);
 
@@ -268,7 +268,7 @@ namespace SharpGLTF.Schema2
         {
             Guard.FilePathMustBeValid(filePath, nameof(filePath));
 
-            var settings = WriteSettings.ForText(filePath);
+            var settings = WriteContext.ForText(filePath);
 
             settings.JsonFormatting = fmt;
 
@@ -286,7 +286,7 @@ namespace SharpGLTF.Schema2
         {
             var dict = new Dictionary<string, BYTES>();
 
-            var settings = WriteSettings.ForText(dict);
+            var settings = WriteContext.ForText(dict);
 
             _Write(settings, fileName, this);
 
@@ -330,20 +330,20 @@ namespace SharpGLTF.Schema2
             Guard.NotNull(stream, nameof(stream));
             Guard.IsTrue(stream.CanWrite, nameof(stream));
 
-            var settings = WriteSettings.ForBinary(stream);
+            var settings = WriteContext.ForBinary(stream);
 
             _Write(settings, "model", this);
         }
 
         /// <summary>
-        /// Writes this <see cref="MODEL"/> to the asset writer in <see cref="WriteSettings"/> configuration.
+        /// Writes this <see cref="MODEL"/> to the asset writer in <see cref="WriteContext"/> configuration.
         /// </summary>
-        /// <param name="settings">A <see cref="WriteSettings"/> to use to write the files.</param>
+        /// <param name="settings">A <see cref="WriteContext"/> to use to write the files.</param>
         /// <param name="baseName">The base name to use for asset files.</param>
         /// <remarks>
         /// Satellite files like buffers and images are also written with the file name formatted as "FILE_{Index}.EXT".
         /// </remarks>
-        public void Write(WriteSettings settings, string baseName)
+        public void Write(WriteContext settings, string baseName)
         {
             Guard.NotNull(settings, nameof(settings));
             _Write(settings, baseName, this);
@@ -359,7 +359,7 @@ namespace SharpGLTF.Schema2
             }
         }
 
-        private static void _Write(WriteSettings settings, string baseName, MODEL model)
+        private static void _Write(WriteContext settings, string baseName, MODEL model)
         {
             Guard.NotNull(settings, nameof(settings));
             Guard.NotNullOrEmpty(baseName, nameof(baseName));
@@ -367,6 +367,8 @@ namespace SharpGLTF.Schema2
 
             model = settings.FilterModel(model);
 
+            foreach (var img in model._images) if (!img._HasContent) throw new Validation.DataException(img, "Content is missing.");
+
             if (settings._UpdateSupportedExtensions) model.UpdateExtensionsSupport();
 
             if (settings.BinaryMode)
@@ -396,9 +398,16 @@ namespace SharpGLTF.Schema2
             for (int i = 0; i < model._images.Count; ++i)
             {
                 var image = model._images[i];
-                var iname = model._images.Count != 1 ? $"{baseName}_{i}" : $"{baseName}";
-                if (settings.ImageWriting != ResourceWriteMode.SatelliteFile) image._WriteToInternal();
-                else image._WriteToSatellite(settings.FileWriter, iname);
+
+                if (settings.ImageWriting != ResourceWriteMode.SatelliteFile)
+                {
+                    image._WriteToInternal();
+                }
+                else
+                {
+                    var iname = model._images.Count != 1 ? $"{baseName}_{i}" : $"{baseName}";
+                    image._WriteToSatellite(settings.FileWriter, iname);
+                }
             }
 
             using (var m = new MemoryStream())

+ 7 - 7
src/SharpGLTF.Core/Validation/ValidationContext.cs

@@ -46,7 +46,7 @@ namespace SharpGLTF.Validation
 
         public void AddSchemaError(ValueLocation location, string message) { AddSchemaError(location.ToString(_Target, message)); }
 
-        public bool TryFixLink(ValueLocation location, string message)
+        public bool TryFixLinkOrError(ValueLocation location, string message)
         {
             if (TryFix) AddLinkWarning(location.ToString(_Target, message));
             else AddLinkError(location.ToString(_Target, message));
@@ -54,7 +54,7 @@ namespace SharpGLTF.Validation
             return TryFix;
         }
 
-        public bool TryFixData(ValueLocation location, string message)
+        public bool TryFixDataOrError(ValueLocation location, string message)
         {
             if (TryFix) AddDataWarning(location.ToString(_Target, message));
             else AddDataError(location.ToString(_Target, message));
@@ -247,22 +247,22 @@ namespace SharpGLTF.Validation
             return false;
         }
 
-        public bool TryFixUnitLength(ValueLocation location, System.Numerics.Vector3? value)
+        public bool TryFixUnitLengthOrError(ValueLocation location, System.Numerics.Vector3? value)
         {
             if (!value.HasValue) return false;
             if (!CheckIsFinite(location, value)) return false;
             if (value.Value.IsValidNormal()) return false;
 
-            return TryFixData(location, $"is not of unit length: {value.Value.Length()}.");
+            return TryFixDataOrError(location, $"is not of unit length: {value.Value.Length()}.");
         }
 
-        public bool TryFixTangent(ValueLocation location, System.Numerics.Vector4 tangent)
+        public bool TryFixTangentOrError(ValueLocation location, System.Numerics.Vector4 tangent)
         {
-            if (TryFixUnitLength(location, new System.Numerics.Vector3(tangent.X, tangent.Y, tangent.Z))) return true;
+            if (TryFixUnitLengthOrError(location, new System.Numerics.Vector3(tangent.X, tangent.Y, tangent.Z))) return true;
 
             if (tangent.W == 1 || tangent.W == -1) return false;
 
-            return TryFixData(location, $"has invalid value: {tangent.W}. Must be 1.0 or -1.0.");
+            return TryFixDataOrError(location, $"has invalid value: {tangent.W}. Must be 1.0 or -1.0.");
         }
 
         public void CheckIsInRange(ValueLocation location, System.Numerics.Vector4 v, float minInclusive, float maxInclusive)

+ 2 - 0
src/SharpGLTF.Core/Validation/ValidationResult.cs

@@ -45,6 +45,8 @@ namespace SharpGLTF.Validation
 
         #region API
 
+        public ValidationContext GetContext() { return new ValidationContext(this, _Root); }
+
         public ValidationContext GetContext(TARGET target) { return new ValidationContext(this, target); }
 
         public void AddWarning(ModelException ex)

+ 1 - 1
tests/SharpGLTF.Tests/Plotting.cs

@@ -295,7 +295,7 @@ namespace SharpGLTF
 
             points.DrawToFile(fileName);
 
-            NUnit.Framework.TestContext.AddTestAttachment(fileName);
+            if (System.IO.File.Exists(fileName)) NUnit.Framework.TestContext.AddTestAttachment(fileName);
         }
 
         public static void AttachToCurrentTest(this IEnumerable<Plotting.Point2Series> series, string fileName)

+ 10 - 0
tests/SharpGLTF.Tests/Schema2/LoadAndSave/LoadSpecialModelsTest.cs

@@ -24,6 +24,16 @@ namespace SharpGLTF.Schema2.LoadAndSave
 
         #endregion
 
+        public void LoadWithCustomImageLoader()
+        {
+            TestContext.CurrentContext.AttachShowDirLink();
+
+            
+
+            // load Polly model
+            var model = ModelRoot.Load(TestFiles.GetPollyFileModelPath());
+        }
+
         [Test(Description = "Example of traversing the visual tree all the way to individual vertices and indices")]
         public void LoadPollyModel()
         {

+ 1 - 3
tests/SharpGLTF.Tests/TestFiles.cs

@@ -61,9 +61,7 @@ namespace SharpGLTF
         private static readonly string _BabylonJsPlaygroundDir;
         private static readonly string _GeneratedModelsDir;
 
-        private static readonly string[] _BabylonJsInvalidFiles = { };
-
-        
+        private static readonly string[] _BabylonJsInvalidFiles = { };        
 
         #endregion
 

+ 31 - 0
tests/SharpGLTF.Tests/Validation/TryFixTests.cs

@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+using NUnit.Framework;
+
+namespace SharpGLTF.Validation
+{
+
+    public class TryFixTests
+    {
+        /*
+        [Test]
+        public void TryFixAnimationWithInvalidByteStride()
+        {
+            var path = System.IO.Path.Combine(TestFiles.RootDirectory, "teascroll_clubhouse_-_fountain_prop", "scene.gltf");
+
+            Assert.Throws<LinkException>(() => Schema2.ModelRoot.Load(path));
+
+            var mdl = Schema2.ModelRoot.Load(path, ValidationMode.TryFix);
+
+            var vcontext = new ValidationResult(mdl, ValidationMode.Strict, true);
+            mdl.ValidateReferences(vcontext.GetContext());
+            mdl.Validate(vcontext.GetContext());
+
+            
+            mdl.WriteGLB();
+        }*/
+
+    }
+}