Browse Source

improving error checking

Vicente Penades 6 years ago
parent
commit
b93ca7876f

+ 60 - 0
src/Shared/Guard.cs

@@ -87,6 +87,14 @@ namespace SharpGLTF
 
         #region comparison
 
+        public static void MustBeEqualTo<TValue>(TValue value, TValue expected, string parameterName)
+                    where TValue : IComparable<TValue>
+        {
+            if (value.CompareTo(expected) == 0) return;
+
+            throw new ArgumentOutOfRangeException(parameterName, $"{parameterName} {value} must be equal to {expected}.");
+        }
+
         public static void MustBePositiveAndMultipleOf(int value, int padding, string parameterName, string message = "")
         {
             if (value < 0)
@@ -180,4 +188,56 @@ namespace SharpGLTF
 
         #endregion
     }
+
+    [DebuggerStepThrough]
+    internal static class GuardAll
+    {
+        public static void NotNull<T>(IEnumerable<T> collection, string parameterName, string message = "")
+        {
+            Guard.NotNull(collection, nameof(collection));
+            foreach (var val in collection) Guard.NotNull(val, parameterName, message);
+        }
+
+        public static void MustBeEqualTo<TValue>(IEnumerable<TValue> collection, TValue expected, string parameterName)
+            where TValue : IComparable<TValue>
+        {
+            Guard.NotNull(collection, nameof(collection));
+            foreach (var val in collection) Guard.MustBeEqualTo(val, expected, parameterName);
+        }
+
+        public static void MustBeGreaterThan<TValue>(IEnumerable<TValue> collection, TValue minExclusive, string parameterName)
+            where TValue : IComparable<TValue>
+        {
+            Guard.NotNull(collection, nameof(collection));
+            foreach (var val in collection) Guard.MustBeGreaterThan(val, minExclusive, parameterName);
+        }
+
+        public static void MustBeLessThan<TValue>(IEnumerable<TValue> collection, TValue maxExclusive, string parameterName)
+            where TValue : IComparable<TValue>
+        {
+            Guard.NotNull(collection, nameof(collection));
+            foreach (var val in collection) Guard.MustBeLessThan(val, maxExclusive, parameterName);
+        }
+
+        public static void MustBeLessThanOrEqualTo<TValue>(IEnumerable<TValue> collection, TValue maxInclusive, string parameterName)
+            where TValue : IComparable<TValue>
+        {
+            Guard.NotNull(collection, nameof(collection));
+            foreach (var val in collection) Guard.MustBeLessThanOrEqualTo(val, maxInclusive, parameterName);
+        }
+
+        public static void MustBeGreaterThanOrEqualTo<TValue>(IEnumerable<TValue> collection, TValue minInclusive, string parameterName)
+            where TValue : IComparable<TValue>
+        {
+            Guard.NotNull(collection, nameof(collection));
+            foreach (var val in collection) Guard.MustBeGreaterThanOrEqualTo(val, minInclusive, parameterName);
+        }
+
+        public static void MustBeBetweenOrEqualTo<TValue>(IEnumerable<TValue> collection, TValue minInclusive, TValue maxInclusive, string parameterName)
+            where TValue : IComparable<TValue>
+        {
+            Guard.NotNull(collection, nameof(collection));
+            foreach (var val in collection) Guard.MustBeBetweenOrEqualTo(val, minInclusive, maxInclusive, parameterName);
+        }
+    }
 }

+ 1 - 1
src/SharpGLTF.Core/IO/JsonSerializable.cs

@@ -18,7 +18,7 @@ namespace SharpGLTF.IO
 
             Validate(result);
 
-            return result.Exceptions;
+            return result.Errors;
         }
 
         internal virtual void Validate(Validation.ValidationContext result)

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

@@ -331,26 +331,41 @@ namespace SharpGLTF.Schema2
             }
         }
 
+        #endregion
+
+        #region validation
+
         internal override void Validate(Validation.ValidationContext result)
         {
             base.Validate(result);
 
-            if (!_bufferView.HasValue) { result.AddError(this, $"BufferView index missing"); return; }
-            if (_bufferView < 0 || _bufferView >= LogicalParent.LogicalBufferViews.Count) result.AddError(this, $"BufferView index out of range");
+            result.CheckIsDefined(this, "BufferView", _bufferView);
+            result.CheckIndex(this, "BufferView", _bufferView, this.LogicalParent.LogicalBufferViews.Count);
 
             if (_count < _countMinimum) result.AddError(this, $"Count is out of range");
             if (_byteOffset < 0) result.AddError(this, $"ByteOffset is out of range");
 
+            if (this.Normalized)
+            {
+                if (this._componentType != EncodingType.UNSIGNED_BYTE && this._componentType != EncodingType.UNSIGNED_SHORT)
+                {
+                    result.AddDataError(this, Validation.ErrorCodes.ACCESSOR_NORMALIZED_INVALID);
+                }
+            }
+
             if (SourceBufferView.DeviceBufferTarget == BufferMode.ARRAY_BUFFER)
             {
                 var len = Encoding.ByteLength() * Dimensions.DimCount();
-                if (len > 0 && (len & 3) != 0) result.AddError(this, $"Expected length to be multiple of 4, found {len}");
+                result.CheckMultipleOf(this, "Encoding", len, 4);
             }
 
             if (SourceBufferView.DeviceBufferTarget == BufferMode.ELEMENT_ARRAY_BUFFER)
             {
-                var len = Encoding.ByteLength() * Dimensions.DimCount();
-                if (len != 1 && len != 2 && len != 4) result.AddError(this, $"Expected length to be 1, 2 or 4, found {len}");
+                if (this.Normalized) result.AddDataError(this, "Normalized", Validation.ErrorCodes.ACCESSOR_NORMALIZED_INVALID);
+                if (this.Dimensions != DimensionType.SCALAR) result.AddDataError(this, "Dimensions", Validation.ErrorCodes.VALUE_MULTIPLE_OF, Encoding.ByteLength() * Dimensions.DimCount(), Encoding.ByteLength());
+                if (this.Encoding != EncodingType.UNSIGNED_BYTE && this.Encoding != EncodingType.UNSIGNED_INT && this.Encoding != EncodingType.UNSIGNED_SHORT) result.AddDataError(this, "Encoding", Validation.ErrorCodes.VALUE_MULTIPLE_OF, Encoding.ByteLength() * Dimensions.DimCount(), Encoding.ByteLength());
+
+                // if this.Encoding != EncodingType.UNSIGNED_BYTE > warning, no longer valid.
             }
         }
 
@@ -396,6 +411,16 @@ namespace SharpGLTF.Schema2
 
         internal void ValidateIndices(Validation.ValidationContext result, uint vertexCount, PrimitiveType drawingType)
         {
+            // if (SourceBufferView.DeviceBufferTarget == BufferMode.ARRAY_BUFFER) // error, this must be a ELEMENT_ARRAY_BUFFER
+
+            if (this.Normalized) result.AddDataError(this, Validation.ErrorCodes.MESH_PRIMITIVE_INDICES_ACCESSOR_INVALID_FORMAT, this.Normalized, false);
+
+            if (Encoding != EncodingType.UNSIGNED_BYTE &&
+                Encoding != EncodingType.UNSIGNED_SHORT &&
+                Encoding != EncodingType.UNSIGNED_INT) result.AddDataError(this, Validation.ErrorCodes.MESH_PRIMITIVE_INDICES_ACCESSOR_INVALID_FORMAT, this.Encoding, EncodingType.UNSIGNED_BYTE, EncodingType.UNSIGNED_SHORT, EncodingType.UNSIGNED_INT);
+
+            if (Dimensions != DimensionType.SCALAR) result.AddDataError(this, Validation.ErrorCodes.MESH_PRIMITIVE_INDICES_ACCESSOR_INVALID_FORMAT, this.Dimensions, DimensionType.SCALAR);
+
             switch (drawingType)
             {
                 case PrimitiveType.LINE_LOOP:
@@ -425,10 +450,7 @@ namespace SharpGLTF.Schema2
 
             for (int i = 0; i < indices.Count; ++i)
             {
-                var idx = indices[i];
-
-                if (idx == restart_value) result.AddError(this, $"Index[{i}] value {idx} is invalid PRIMITIVE RESTART value");
-                else if (idx >= vertexCount) result.AddError(this, $"Index[{i}] value {idx} is out of range {0}-{vertexCount}");
+                result.CheckVertexIndex(this, i, indices[i], vertexCount, restart_value);
             }
         }
 
@@ -439,8 +461,7 @@ namespace SharpGLTF.Schema2
             for (int i = 0; i < positions.Count; ++i)
             {
                 var pos = positions[i];
-
-                if (!pos._IsFinite()) result.AddError(this, $"POSITION[{i}] value {pos} has non finite values");
+                result.CheckDataIsFinite(this, i, pos);
             }
         }
 
@@ -451,8 +472,8 @@ namespace SharpGLTF.Schema2
             for (int i = 0; i < normals.Count; ++i)
             {
                 var nrm = normals[i];
-
-                if (!nrm.IsValidNormal()) result.AddError(this, $"NORMAL[{i}] value {nrm} is invalid");
+                result.CheckDataIsFinite(this, i, nrm);
+                result.CheckDataIsUnitLength(this, i, nrm);
             }
         }
 
@@ -464,54 +485,33 @@ namespace SharpGLTF.Schema2
             {
                 var tgt = tangents[i];
 
-                if (!tgt._IsFinite()) result.AddError(this, $"TANGENT[{i}] value {tgt} has non finite values");
-
-                var len = new Vector3(tgt.X, tgt.Y, tgt.Z).Length();
-
-                if (len < 0.99f || len > 1.01f) result.AddError(this, $"TANGENT[{i}] length {len} is not unit length");
-
-                if (tgt.W != 1 && tgt.W != -1) result.AddError(this, $"TANGENT[{i}].W {tgt.W} has invalid value");
+                result.CheckDataIsFinite(this, i, tgt);
+                result.CheckDataIsUnitLength(this, i, new Vector3(tgt.X, tgt.Y, tgt.Z));
+                result.CheckDataIsValidSign(this, i, tgt.W);
             }
         }
 
         internal void ValidateJoints(Validation.ValidationContext result, int jwset, int jointsCount)
         {
-            var jj = this.AsVector4Array();
+            var joints = this.AsVector4Array();
 
-            void _CheckJoint(Validation.ValidationContext r, float v, int idx, string n)
+            for (int i = 0; i < joints.Count; ++i)
             {
-                if (!v._IsFinite()) result.AddError(this, $"JOINTS_{jwset}[{idx}].{n} value {v} is not finite");
-                if ((v % 1) != 0) result.AddError(this, $"JOINTS_{jwset}[{idx}].{n} value {v} should be a round value");
-                if (v < 0 || v >= jointsCount) result.AddError(this, $"JOINTS_{jwset}[{idx}].{n} value {v} is out of range 0-{jointsCount}");
-            }
-
-            for (int i = 0; i < jj.Count; ++i)
-            {
-                var jjjj = jj[i];
-                _CheckJoint(result, jjjj.X, i, "X");
-                _CheckJoint(result, jjjj.Y, i, "Y");
-                _CheckJoint(result, jjjj.Z, i, "Z");
-                _CheckJoint(result, jjjj.W, i, "W");
+                var jjjj = joints[i];
+                result.CheckDataIsFinite(this, i, jjjj);
+                result.CheckDataIsInRange(this, i, jjjj, 0, jointsCount-1);
             }
         }
 
         internal void ValidateWeights(Validation.ValidationContext result, int jwset)
         {
-            var ww = this.AsVector4Array();
-
-            void _CheckWeight(Validation.ValidationContext r, float v, int idx, string n)
-            {
-                if (!v._IsFinite()) result.AddError(this, $"WEIGHTS_{jwset}[{idx}].{n} value {v} is not finite");
-                if (v < 0 || v > 1) result.AddError(this, $"WEIGHTS_{jwset}[{idx}].{n} value {v} is out of range 0-1");
-            }
+            var weights = this.AsVector4Array();
 
-            for (int i = 0; i < ww.Count; ++i)
+            for (int i = 0; i < weights.Count; ++i)
             {
-                var wwww = ww[i];
-                _CheckWeight(result, wwww.X, i, "X");
-                _CheckWeight(result, wwww.Y, i, "Y");
-                _CheckWeight(result, wwww.Z, i, "Z");
-                _CheckWeight(result, wwww.W, i, "W");
+                var wwww = weights[i];
+                result.CheckDataIsFinite(this, i, wwww);
+                result.CheckDataIsInRange(this, i, wwww, 0, 1);
 
                 // theoretically, the sum of all the weights should give 1, ASSUMING there's only one weight set.
                 // but in practice, that seems not to be true.

+ 1 - 1
src/SharpGLTF.Core/Schema2/gltf.Asset.cs

@@ -76,7 +76,7 @@ namespace SharpGLTF.Schema2
             if (Version < MINVERSION) result.AddError(this, $"Minimum supported version is {MINVERSION} but found:{MinVersion}");
             if (MinVersion > MAXVERSION) result.AddError(this, $"Maximum supported version is {MAXVERSION} but found:{MinVersion}");
 
-            if (MinVersion > Version) result.AddSemanticError(this, $"Asset minVersion {MinVersion} is greater than version {Version}.");
+            if (MinVersion > Version) result.AddSemanticWarning(this, Validation.ErrorCodes.ASSET_MIN_VERSION_GREATER_THAN_VERSION, MinVersion, Version);
         }
 
         private string _GetExtraInfo(string key)

+ 15 - 0
src/SharpGLTF.Core/Schema2/gltf.BufferView.cs

@@ -125,6 +125,21 @@ namespace SharpGLTF.Schema2
         }
 
         #endregion
+
+        #region validation
+
+        internal override void Validate(Validation.ValidationContext result)
+        {
+            base.Validate(result);
+
+            if (this.FindAccessors().Skip(1).Any())
+            {
+                if (!_byteStride.HasValue) result.AddLinkError(this, Validation.ErrorCodes.MESH_PRIMITIVE_ACCESSOR_WITHOUT_BYTESTRIDE);
+            }
+        }
+
+        #endregion
+
     }
 
     public partial class ModelRoot

+ 1 - 1
src/SharpGLTF.Core/Schema2/gltf.ExtraProperties.cs

@@ -125,7 +125,7 @@ namespace SharpGLTF.Schema2
 
             if (this._extras != null)
             {
-                if (!IO.JsonUtils.IsSerializable(this._extras)) result.InvalidJson(this, "Extras");
+                result.CheckJsonSerializable(this, "Extras", this._extras);
             }
         }
 

+ 6 - 2
src/SharpGLTF.Core/Schema2/gltf.Mesh.cs

@@ -88,6 +88,10 @@ namespace SharpGLTF.Schema2
             return mp;
         }
 
+        #endregion
+
+        #region Validation
+
         internal override void Validate(Validation.ValidationContext result)
         {
             base.Validate(result);
@@ -101,8 +105,8 @@ namespace SharpGLTF.Schema2
                 .Select(item => item.MorphTargetsCount)
                 .Distinct();
 
-            if (morphTargetsCount.Count() != 1) result.AddSemanticError(this, Validation.SemanticErrors.MESH_PRIMITIVES_UNEQUAL_TARGETS_COUNT);
-            if (_weights.Count != 0 && morphTargetsCount.First() != _weights.Count) result.AddSemanticError(this, Validation.SemanticErrors.MESH_INVALID_WEIGHTS_COUNT, _weights.Count, morphTargetsCount.First());
+            if (morphTargetsCount.Count() != 1) result.AddSemanticError(this, Validation.ErrorCodes.MESH_PRIMITIVES_UNEQUAL_TARGETS_COUNT);
+            if (_weights.Count != 0 && morphTargetsCount.First() != _weights.Count) result.AddSemanticError(this, Validation.ErrorCodes.MESH_INVALID_WEIGHTS_COUNT, _weights.Count, morphTargetsCount.First());
         }
 
         internal void ValidateSkinning(Validation.ValidationContext result, int jointsCount)

+ 42 - 0
src/SharpGLTF.Core/Validation/ErrorCodes.Data.cs

@@ -0,0 +1,42 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace SharpGLTF.Validation
+{
+    static partial class ErrorCodes
+    {
+        // INFORMATION
+
+        public const string ACCESSOR_INDEX_TRIANGLE_DEGENERATE = "Indices accessor contains {0} degenerate triangles.";
+        public const string DATA_URI_GLB = "Data URI is used in GLB container.";
+        public const string IMAGE_NPOT_DIMENSIONS = "Image has non-power-of-two dimensions: {0}x{1}.";
+
+        // WARNINGS
+
+        public const string BUFFER_GLB_CHUNK_TOO_BIG = "GLB-stored BIN chunk contains {0} extra padding byte (s).";
+        public const string IMAGE_UNRECOGNIZED_FORMAT = "Image format not recognized.";
+
+        // ERRORS
+
+        public const string ACCESSOR_ANIMATION_INPUT_NEGATIVE = "Animation input accessor element at index {0} is negative: {1}.";
+        public const string ACCESSOR_ANIMATION_INPUT_NON_INCREASING = "Animation input accessor element at index {0} is less than or equal to previous: {1} <= {2}.";
+        public const string ACCESSOR_ELEMENT_OUT_OF_MAX_BOUND = "Accessor contains {0} element(s) greater than declared maximum value {1}.";
+        public const string ACCESSOR_ELEMENT_OUT_OF_MIN_BOUND = "Accessor contains {0} element(s) less than declared minimum value {1}.";
+        public const string ACCESSOR_INDECOMPOSABLE_MATRIX = "Matrix element at index {0} is not decomposable to TRS.";
+        public const string ACCESSOR_INDEX_OOB = "Indices accessor element at index {0} has vertex index {1} that exceeds number of available vertices {2}.";
+        public const string ACCESSOR_INDEX_PRIMITIVE_RESTART = "Indices accessor contains primitive restart value ({0}) at index {1}.";
+        public const string ACCESSOR_INVALID_FLOAT = "Accessor element at index {0} is NaN or Infinity.";
+        public const string ACCESSOR_INVALID_SIGN = "Accessor element at index {0} has invalid w component: {1}. Must be 1.0 or -1.0.";
+        public const string ACCESSOR_MAX_MISMATCH = "Declared maximum value for this component ({0}) does not match actual maximum({1}).";
+        public const string ACCESSOR_MIN_MISMATCH = "Declared minimum value for this component({0}) does not match actual minimum({1}).";
+        public const string ACCESSOR_NON_UNIT = "Accessor element at index {0} is not of unit length: {1}.";
+        public const string ACCESSOR_SPARSE_INDEX_OOB = "Accessor sparse indices element at index {0} is greater than or equal to the number of accessor elements: {1} >= {2}.";
+        public const string ACCESSOR_SPARSE_INDICES_NON_INCREASING = "Accessor sparse indices element at index {0} is less than or equal to previous: {1} <= {2}.";
+        public const string BUFFER_EMBEDDED_BYTELENGTH_MISMATCH = "Actual data length {0} is not equal to the declared buffer byteLength {1}.";
+        public const string BUFFER_EXTERNAL_BYTELENGTH_MISMATCH = "Actual data length {0} is less than the declared buffer byteLength {1}.";
+        public const string IMAGE_DATA_INVALID = "Image data is invalid. {0}";
+        public const string IMAGE_MIME_TYPE_INVALID = "Recognized image format '{0}' does not match declared image format '{1}'.";
+        public const string IMAGE_UNEXPECTED_EOS = "Unexpected end of image stream.";
+    }
+}

+ 60 - 0
src/SharpGLTF.Core/Validation/ErrorCodes.Link.cs

@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace SharpGLTF.Validation
+{
+    static partial class ErrorCodes
+    {
+        // INFORMATION
+
+        public const string MESH_PRIMITIVE_UNUSED_TEXCOORD = "Material does not use texture coordinates sets with indices ('%a', '%b', '%c').";
+        public const string UNUSED_OBJECT = "This object may be unused.";
+
+        // WARNINGS
+
+        public const string MESH_PRIMITIVE_INCOMPATIBLE_MODE = "Number of vertices or indices({0}) is not compatible with used drawing mode('{1}').";
+        public const string NODE_SKINNED_MESH_WITHOUT_SKIN = "Node uses skinned mesh, but has no skin defined.";
+        public const string UNSUPPORTED_EXTENSION = "Cannot validate an extension as it is not supported by the validator: '{0}'.";
+
+        // ERRORS
+
+        public const string ACCESSOR_SMALL_BYTESTRIDE = "Referenced bufferView's byteStride value {0} is less than accessor element's length {1}.";
+        public const string ACCESSOR_TOO_LONG = "Accessor(offset: {0}, length: {1}) does not fit referenced bufferView[% 3] length %4.";
+        public const string ACCESSOR_TOTAL_OFFSET_ALIGNMENT = "Accessor's total byteOffset {0} isn't a multiple of componentType length {1}.";
+        public const string ACCESSOR_USAGE_OVERRIDE = "Override of previously set accessor usage.Initial: '{0}', new: '{1}'.";
+        public const string ANIMATION_CHANNEL_TARGET_NODE_MATRIX = "Animation channel cannot target TRS properties of node with defined matrix.";
+        public const string ANIMATION_CHANNEL_TARGET_NODE_WEIGHTS_NO_MORPHS = "Animation channel cannot target WEIGHTS when mesh does not have morph targets.";
+        public const string ANIMATION_DUPLICATE_TARGETS = "Animation channel has the same target as channel {0}. 	Error";
+        public const string ANIMATION_SAMPLER_INPUT_ACCESSOR_INVALID_FORMAT = "Invalid Animation sampler input accessor format '{0}'. Must be one of ('%a', '%b', '%c').";
+        public const string ANIMATION_SAMPLER_INPUT_ACCESSOR_TOO_FEW_ELEMENTS = "Animation sampler output accessor with '{0}' interpolation must have at least {1} elements.Got {2}.";
+        public const string ANIMATION_SAMPLER_INPUT_ACCESSOR_WITHOUT_BOUNDS = "accessor.min and accessor.max must be defined for animation input accessor.";
+        public const string ANIMATION_SAMPLER_OUTPUT_ACCESSOR_INVALID_COUNT = "Animation sampler output accessor of count {0} expected.Found {1}.";
+        public const string ANIMATION_SAMPLER_OUTPUT_ACCESSOR_INVALID_FORMAT = "Invalid animation sampler output accessor format '{0}' for path '{2}'. Must be one of('%a', '%b', '%c').";
+        public const string ANIMATION_SAMPLER_OUTPUT_INTERPOLATION = "The same output accessor cannot be used both for spline and linear data.";
+        public const string BUFFER_MISSING_GLB_DATA = "Buffer refers to an unresolved GLB binary chunk.";
+        public const string BUFFER_NON_FIRST_GLB = "Buffer referring to GLB binary chunk must be the first.";
+        public const string BUFFER_VIEW_TARGET_OVERRIDE = "Override of previously set bufferView target or usage. Initial: '{0}', new: '{1}'.";
+        public const string BUFFER_VIEW_TOO_LONG = "BufferView does not fit buffer({0}) byteLength({1}).";
+        public const string INVALID_IBM_ACCESSOR_COUNT = "Accessor of count {0} expected.Found {1}.";
+        public const string MESH_PRIMITIVE_ACCESSOR_UNALIGNED = "Vertex attribute data must be aligned to 4-byte boundaries.";
+        public const string MESH_PRIMITIVE_ACCESSOR_WITHOUT_BYTESTRIDE = "bufferView.byteStride must be defined when two or more accessors use the same buffer view.";
+        public const string MESH_PRIMITIVE_ATTRIBUTES_ACCESSOR_INVALID_FORMAT = "Invalid accessor format '{0}' for this attribute semantic. Must be one of ('%a', '%b', '%c').";
+        public const string MESH_PRIMITIVE_INDICES_ACCESSOR_INVALID_FORMAT = "Invalid indices accessor format '{0}'. Must be one of('{1}', '{2}', '{3}').";
+        public const string MESH_PRIMITIVE_INDICES_ACCESSOR_WITH_BYTESTRIDE = "bufferView.byteStride must not be defined for indices accessor.";
+        public const string MESH_PRIMITIVE_MORPH_TARGET_INVALID_ATTRIBUTE_COUNT = "Base accessor has different count.";
+        public const string MESH_PRIMITIVE_MORPH_TARGET_NO_BASE_ACCESSOR = "No base accessor for this attribute semantic.";
+        public const string MESH_PRIMITIVE_POSITION_ACCESSOR_WITHOUT_BOUNDS = "accessor.min and accessor.max must be defined for POSITION attribute accessor.";
+        public const string MESH_PRIMITIVE_TOO_FEW_TEXCOORDS = "Material is incompatible with mesh primitive: Texture binding '{0}' needs 'TEXCOORD_{1}' attribute.";
+        public const string MESH_PRIMITIVE_UNEQUAL_ACCESSOR_COUNT = "All accessors of the same primitive must have the same count.";
+        public const string NODE_LOOP = "Node is a part of a node loop.";
+        public const string NODE_PARENT_OVERRIDE = "Value overrides parent of node {0}.";
+        public const string NODE_SKIN_WITH_NON_SKINNED_MESH = "Node has skin defined, but mesh has no joints data.";
+        public const string NODE_WEIGHTS_INVALID = "The length of weights array ({0}) does not match the number of morph targets({1}).";
+        public const string SCENE_NON_ROOT_NODE = "Node {0} is not a root node.";
+        public const string SKIN_IBM_INVALID_FORMAT = "Invalid IBM accessor format '{0}'. Must be one of ('%a', '%b', '%c').";
+        public const string UNDECLARED_EXTENSION = "Extension was not declared in extensionsUsed.";
+        public const string UNEXPECTED_EXTENSION_OBJECT = "Unexpected location for this extension.";
+        public const string UNRESOLVED_REFERENCE = "Unresolved reference: {0}.";
+    }
+}

+ 31 - 0
src/SharpGLTF.Core/Validation/ErrorCodes.Schema.cs

@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace SharpGLTF.Validation
+{
+    static partial class ErrorCodes
+    {
+        // WARNINGS
+
+        public const string UNEXPECTED_PROPERTY = "Unexpected property.";
+        public const string VALUE_NOT_IN_LIST = "Invalid value '{0}'. Valid values are ('%a', '%b', '%c').";
+
+        // ERRRORS
+
+        public const string ARRAY_LENGTH_NOT_IN_LIST = "Invalid array length {0}. Valid lengths are: ('%a', '%b', '%c').";
+        public const string ARRAY_TYPE_MISMATCH = "Type mismatch. Array element '{0}' is not a '{1}'.";
+        public const string DUPLICATE_ELEMENTS = "Duplicate element.";
+        public const string EMPTY_ENTITY = "Entity cannot be empty.";
+        public const string INVALID_INDEX = "Index must be a non-negative integer.";
+        public const string INVALID_JSON = "Invalid JSON data.Parser output: {0}";
+        public const string INVALID_URI = "Invalid URI '{0}'. Parser output: {1}";
+        public const string ONE_OF_MISMATCH = "Exactly one of ('{0}', '{1}', '{2}', '{3}') properties must be defined.";
+        public const string PATTERN_MISMATCH = "Value '{0}' does not match regexp pattern '{1}'.";
+        public const string TYPE_MISMATCH = "Type mismatch. Property value '{0}' is not a '{1}'.";
+        public const string UNDEFINED_PROPERTY = "Property '{0}' must be defined.";
+        public const string UNSATISFIED_DEPENDENCY = "Dependency failed. '{0}' must be defined.";
+        public const string VALUE_MULTIPLE_OF = "Value {0} is not a multiple of {1}.";
+        public const string VALUE_NOT_IN_RANGE = "Value {0} is out of range.";
+    }
+}

+ 1 - 1
src/SharpGLTF.Core/Validation/SemanticErrors.cs → src/SharpGLTF.Core/Validation/ErrorCodes.Semantic.cs

@@ -4,7 +4,7 @@ using System.Text;
 
 namespace SharpGLTF.Validation
 {
-    static class SemanticErrors
+    static partial class ErrorCodes
     {
         #pragma warning disable SA1310 // Field names should not contain underscore
 

+ 88 - 16
src/SharpGLTF.Core/Validation/ValidationContext.cs

@@ -16,33 +16,60 @@ namespace SharpGLTF.Validation
 
         #region data
 
-        private readonly List<Exception> _Exceptions = new List<Exception>();
+        private readonly List<Exception> _Errors = new List<Exception>();
+        private readonly List<Exception> _Warnings = new List<Exception>();
 
-        public IEnumerable<Exception> Exceptions => _Exceptions;
+        public IEnumerable<Exception> Errors => _Errors;
 
-        public bool HasErrors => _Exceptions.Count > 0;
+        public bool HasErrors => _Errors.Count > 0;
 
         #endregion
 
         #region errors
 
+        private void _AddError(Exception ex)
+        {
+            throw ex;
+            _Errors.Add(ex);
+        }
+
         public void AddError(TARGET target, String message)
         {
-            _Exceptions.Add(new ModelException(target, message));
+            _AddError(new ModelException(target, message));
         }
 
         #endregion
 
         #region schema errors
 
-        public void AddSchemaError(TARGET target, String message)
+        public void CheckIsDefined<T>(TARGET target, string property, T? value)
+            where T : struct
+        {
+            if (!value.HasValue) AddSchemaError(target, property, ErrorCodes.UNDEFINED_PROPERTY);
+        }
+
+        public void CheckIndex(TARGET target, string property, int? index, int maxExclusive)
+        {
+            if (index.HasValue)
+            {
+                if (index.Value < 0) AddSchemaError(target, property, ErrorCodes.INVALID_INDEX, index);
+                if (index.Value >= maxExclusive) AddSchemaError(target, property, ErrorCodes.VALUE_NOT_IN_RANGE, index);
+            }
+        }
+
+        public void CheckMultipleOf(TARGET target, string property,  int value, int multiple)
+        {
+            if ((value % multiple) != 0) AddSchemaError(target, property, ErrorCodes.VALUE_MULTIPLE_OF, value, multiple);
+        }
+
+        public void CheckJsonSerializable(TARGET target, string property, Object value)
         {
-            _Exceptions.Add(new SchemaException(target, message));
+            if (!IO.JsonUtils.IsSerializable(value)) AddSchemaError(target, property, ErrorCodes.INVALID_JSON, string.Empty);
         }
 
-        public void InvalidJson(TARGET target, string message)
+        public void AddSchemaError(TARGET target, string property, String format, params object[] args)
         {
-            AddSchemaError(target, $"Invalid JSON data. Parser output: {message}");
+            _AddError(new SchemaException(target, property + " " + String.Format(format, args)));
         }
 
         #endregion
@@ -51,37 +78,82 @@ namespace SharpGLTF.Validation
 
         public void AddSemanticError(TARGET target, String message)
         {
-            _Exceptions.Add(new SemanticException(target, message));
+            _Errors.Add(new SemanticException(target, message));
+        }
+
+        public void AddSemanticWarning(TARGET target, String format, params object[] args)
+        {
+            _Warnings.Add(new SemanticException(target, String.Format(format, args)));
         }
 
         public void AddSemanticError(TARGET target, String format, params object[] args)
         {
-            _Exceptions.Add(new SemanticException(target, String.Format(format, args)));
+            _AddError(new SemanticException(target, String.Format(format, args)));
         }
 
         #endregion
 
         #region data errors
 
-        public void AddDataError(TARGET target, String message)
+        public void CheckVertexIndex(Schema2.Accessor target, int index, UInt32 vertexIndex, UInt32 vertexCount, UInt32 vertexRestart)
         {
-            _Exceptions.Add(new DataException(target, message));
+            if (vertexIndex == vertexRestart) AddDataError(target, ErrorCodes.ACCESSOR_INDEX_PRIMITIVE_RESTART, index, vertexIndex);
+            else if (vertexIndex >= vertexCount) AddDataError(target, ErrorCodes.ACCESSOR_INDEX_OOB, index, vertexIndex, vertexCount);
         }
 
-        #endregion
+        public void CheckDataIsFinite(Schema2.Accessor target, int index, System.Numerics.Vector3 v)
+        {
+            if (!v._IsFinite()) AddDataError(target, ErrorCodes.ACCESSOR_INVALID_FLOAT, index);
+        }
 
-        #region link errors
+        public void CheckDataIsFinite(Schema2.Accessor target, int index, System.Numerics.Vector4 v)
+        {
+            if (!v._IsFinite()) AddDataError(target, ErrorCodes.ACCESSOR_INVALID_FLOAT, index);
+        }
+
+        public void CheckDataIsUnitLength(Schema2.Accessor target, int index, System.Numerics.Vector3 v)
+        {
+            if (!v.IsValidNormal()) AddDataError(target, ErrorCodes.ACCESSOR_NON_UNIT, index, v.Length());
+        }
+
+        public void CheckDataIsInRange(Schema2.Accessor target, int index, System.Numerics.Vector4 v, float minInclusive, float maxInclusive)
+        {
+            CheckDataIsInRange(target, index, v.X, minInclusive, maxInclusive);
+            CheckDataIsInRange(target, index, v.Y, minInclusive, maxInclusive);
+            CheckDataIsInRange(target, index, v.Z, minInclusive, maxInclusive);
+            CheckDataIsInRange(target, index, v.W, minInclusive, maxInclusive);
+        }
+
+        public void CheckDataIsInRange(Schema2.Accessor target, int index, float v, float minInclusive, float maxInclusive)
+        {
+            if (v < minInclusive) AddDataError(target, ErrorCodes.ACCESSOR_ELEMENT_OUT_OF_MIN_BOUND, index, v);
+            if (v > maxInclusive) AddDataError(target, ErrorCodes.ACCESSOR_ELEMENT_OUT_OF_MAX_BOUND, index, v);
+        }
 
-        public void AddLinkError(TARGET target, String message)
+        public void CheckDataIsValidSign(Schema2.Accessor target, int index, float w)
         {
-            _Exceptions.Add(new LinkException(target, message));
+            if (w != 1 && w != -1) AddDataError(target, ErrorCodes.ACCESSOR_INVALID_SIGN, index, w);
         }
 
+        public void AddDataError(TARGET target, String format, params object[] args)
+        {
+            _AddError(new DataException(target, String.Format(format, args)));
+        }
+
+        #endregion
+
+        #region link errors
+
         public void UnsupportedExtensionError(TARGET target, String message)
         {
             AddLinkError(target, message);
         }
 
+        public void AddLinkError(TARGET target, String format, params object[] args)
+        {
+            _AddError(new LinkException(target, String.Format(format, args)));
+        }
+
         #endregion
     }
 }

+ 3 - 4
src/SharpGLTF.Toolkit/Geometry/PackedPrimitiveBuilder.cs

@@ -59,15 +59,14 @@ namespace SharpGLTF.Geometry
                         .ToList();
 
             var vAccessors = new List<Memory.MemoryAccessor>();
+            GuardAll.MustBeEqualTo(vAccessors.Select(item => item.Attribute.ByteOffset), 0, nameof(vAccessors));
+            GuardAll.MustBeEqualTo(vAccessors.Select(item => item.Attribute.ByteStride), 0, nameof(vAccessors));
 
             foreach (var an in attributeNames)
             {
                 var vAccessor = VertexTypes.VertexUtils.CreateVertexMemoryAccessor(srcPrim.Vertices, an, encoding);
                 if (vAccessor == null) continue;
 
-                System.Diagnostics.Debug.Assert(vAccessor.Attribute.ByteOffset == 0);
-                System.Diagnostics.Debug.Assert(vAccessor.Attribute.ByteStride == 0);
-
                 vAccessors.Add(vAccessor);
             }
 
@@ -129,7 +128,7 @@ namespace SharpGLTF.Geometry
             var name = accessor.Attribute.Name;
             if (!name.EndsWith("DELTA", StringComparison.Ordinal)) throw new InvalidOperationException();
 
-            name = name.Substring(0, name.Length - 5);
+            name = name.Replace("DELTA", string.Empty);
 
             var attr = accessor.Attribute;
             attr.Name = name;

+ 1 - 1
tests/SharpGLTF.Tests/Schema2/LoadAndSave/LoadPollyTest.cs

@@ -12,7 +12,7 @@ namespace SharpGLTF.Schema2.LoadAndSave
     /// </summary>
     [TestFixture]
     [Category("Model Load and Save")]
-    public class LoadPollyTest
+    public class LoadSpecialModelsTest
     {
         #region setup