Browse Source

Merge pull request #218 from bertt/add_3dtiles_nodata

add 3d tiles metadata noData and default support
Vicente Penades 1 year ago
parent
commit
036ba944b9

+ 1 - 1
src/SharpGLTF.Ext.3DTiles/Schema2/Ext.Features.cs

@@ -336,7 +336,7 @@ namespace SharpGLTF.Schema2
                     {
                         var expectedTexCoordAttribute = $"TEXCOORD_{texture.TextureCoordinate}";
                         var vertex = _meshPrimitive.GetVertexAccessor(expectedTexCoordAttribute);
-                        var distinctFeatureIds = vertex.AsVector2Array().Count();
+                        var distinctFeatureIds = vertex.AsVector2Array().Count;
 
                         Guard.IsTrue(featureId.FeatureCount == distinctFeatureIds, $"Mismatch between FeatureCount ({featureId.FeatureCount}) and Feature Texture ({distinctFeatureIds})");
 

+ 206 - 81
src/SharpGLTF.Ext.3DTiles/Schema2/Ext.StructuralMetadataRoot.cs

@@ -233,7 +233,6 @@ namespace SharpGLTF.Schema2
                     var regex = "^[a-zA-Z_][a-zA-Z0-9_]*$";
                     Guard.IsTrue(System.Text.RegularExpressions.Regex.IsMatch(Schema.Id, regex), nameof(Schema.Id));
 
-
                     foreach (var _class in Schema.Classes)
                     {
                         Guard.IsTrue(System.Text.RegularExpressions.Regex.IsMatch(_class.Key, regex), nameof(_class.Key));
@@ -244,6 +243,17 @@ namespace SharpGLTF.Schema2
                             {
                                 Guard.MustBeGreaterThanOrEqualTo(property.Value.Count.Value, 2, nameof(property.Value.Count));
                             }
+
+                            if (property.Value.Required)
+                            {
+                                Guard.IsTrue(property.Value.NoData == null, nameof(property.Value.NoData), $"The property '{property.Key}' defines a 'noData' value, but is 'required'");
+                            }
+
+                            if(property.Value.Type == ELEMENTTYPE.SCALAR)
+                            {
+                                // check The 'componentType' must be defined for a property with type 'SCALAR'
+                                Guard.IsTrue(property.Value.ComponentType.HasValue, nameof(property.Value.ComponentType), $"The 'componentType' must be defined for a property '{property.Key}' with type 'SCALAR'");
+                            }
                         }
                     }
                 }
@@ -806,7 +816,6 @@ namespace SharpGLTF.Schema2
 
                 if (elementType == ELEMENTTYPE.ENUM)
                 {
-                    // guard the type of t is an short in case of enum
                     Guard.IsTrue(typeof(T) == typeof(short), nameof(T), $"Enum value type of {LogicalKey} must be short");
                 }
                 else if (elementType == ELEMENTTYPE.SCALAR)
@@ -1227,83 +1236,78 @@ namespace SharpGLTF.Schema2
             #endregion
 
             #region properties
+
             public string Name
             {
                 get => _name;
-                set => _name = value;
             }
 
             public string Description
             {
                 get => _description;
-                set => _description = value;
             }
 
             internal ELEMENTTYPE Type
             {
                 get => _type;
-                set => _type = value;
             }
 
             public string EnumType
             {
                 get => _enumType;
-                // set => _enumType = value;
             }
 
             public DATATYPE? ComponentType
             {
                 get => _componentType;
-                set => _componentType = value;
             }
 
             public bool Required
             {
                 get => _required ?? _requiredDefault;
-                set => _required = value.AsNullable(_requiredDefault);
+            }
+
+            public JsonNode NoData
+            {
+                get => _noData;
             }
 
             public bool Normalized
             {
                 get => _normalized ?? _normalizedDefault;
-                set => _normalized = value.AsNullable(_normalizedDefault);
             }
 
             public bool Array
             {
                 get => _array ?? _arrayDefault;
-                set => _array = value.AsNullable(_arrayDefault);
+                internal set => _array = value.AsNullable(_arrayDefault);
             }
 
             public int? Count
             {
                 get => _count;
-                set => _count = value;
+                internal set => _count = value;
             }
 
             /** Commented out for now, as it is not supported
             public JsonNode Min
             {
                 get => _min;
-                set => _min = value;
             }
 
             public JsonNode Max
             {
                 get => _max;
-                set => _max = value;
             }
 
             public JsonNode Scale
             {
                 get => _scale;
-                set => _scale = value;
             }
 
             public JsonNode Offset
             {
                 get => _offset;
-                set => _offset = value;
             }
             */
 
@@ -1315,167 +1319,288 @@ namespace SharpGLTF.Schema2
 
             public StructuralMetadataClassProperty WithName(string name)
             {
-                Name = name;
+                _name = name;
                 return this;
             }
 
             public StructuralMetadataClassProperty WithDescription(string description)
             {
-                Description = description;
+                _description = description;
                 return this;
             }
 
-            public StructuralMetadataClassProperty WithStringType()
+            public StructuralMetadataClassProperty WithStringType(string noData = null, string defaultValue = null)
             {
-                Type = ElementType.STRING;
+                _type = ElementType.STRING;
+                if (noData != null) _noData = noData;
+                if(defaultValue != null) _default = defaultValue;
                 return this;
             }
 
             public StructuralMetadataClassProperty WithBooleanType()
             {
-                Type = ElementType.BOOLEAN;
+                _type = ElementType.BOOLEAN;
                 return this;
             }
 
-            public StructuralMetadataClassProperty WithUInt8Type()
+            public StructuralMetadataClassProperty WithUInt8Type(byte? noData = null, byte? defaultValue = null)
             {
-                Type = ELEMENTTYPE.SCALAR;
-                ComponentType = DATATYPE.UINT8;
+                _type = ELEMENTTYPE.SCALAR;
+                _componentType = DATATYPE.UINT8;
+                if (noData != null) _noData = noData;
+                if (defaultValue != null) _default = defaultValue;
                 return this;
             }
 
-            public StructuralMetadataClassProperty WithInt8Type()
+            public StructuralMetadataClassProperty WithInt8Type(sbyte? noData = null, sbyte? defaultValue = null)
             {
-                Type = ELEMENTTYPE.SCALAR;
-                ComponentType = DATATYPE.INT8;
+                _type = ELEMENTTYPE.SCALAR;
+                _componentType = DATATYPE.INT8;
+                if (noData != null) _noData = noData;
+                if (defaultValue != null) _default = defaultValue;
                 return this;
             }
 
-            public StructuralMetadataClassProperty WithUInt16Type()
+            public StructuralMetadataClassProperty WithUInt16Type(ushort? noData = null, ushort? defaultValue = null)
             {
-                Type = ELEMENTTYPE.SCALAR;
-                ComponentType = DATATYPE.UINT16;
+                _type = ELEMENTTYPE.SCALAR;
+                _componentType = DATATYPE.UINT16;
+                if (noData != null) _noData = noData;
+                if (defaultValue != null) _default = defaultValue;
                 return this;
             }
 
-            public StructuralMetadataClassProperty WithInt16Type()
+            public StructuralMetadataClassProperty WithInt16Type(short? noData = null, short? defaultValue = null)
             {
-                Type = ELEMENTTYPE.SCALAR;
-                ComponentType = DATATYPE.INT16;
+                _type = ELEMENTTYPE.SCALAR;
+                _componentType = DATATYPE.INT16;
+                if (noData != null) _noData = noData;
+                if (defaultValue != null) _default = defaultValue;
                 return this;
             }
 
-            public StructuralMetadataClassProperty WithUInt32Type()
+            public StructuralMetadataClassProperty WithUInt32Type(uint? noData = null, uint? defaultValue = null)
             {
-                Type = ELEMENTTYPE.SCALAR;
-                ComponentType = DATATYPE.UINT32;
+                _type = ELEMENTTYPE.SCALAR;
+                _componentType = DATATYPE.UINT32;
+                if (noData != null) _noData = noData;
+                if (defaultValue != null) _default = defaultValue;
                 return this;
             }
 
-            public StructuralMetadataClassProperty WithInt32Type()
+            public StructuralMetadataClassProperty WithInt32Type(int? noData = null, int? defaultValue = null)
             {
-                Type = ELEMENTTYPE.SCALAR;
-                ComponentType = DATATYPE.INT32;
+                _type = ELEMENTTYPE.SCALAR;
+                _componentType = DATATYPE.INT32;
+                if (noData != null) _noData = noData;
+                if (defaultValue != null) _default = defaultValue;
                 return this;
             }
 
-            public StructuralMetadataClassProperty WithUInt64Type()
+            public StructuralMetadataClassProperty WithUInt64Type(ulong? noData = null, ulong? defaultValue = null)
             {
-                Type = ELEMENTTYPE.SCALAR;
-                ComponentType = DATATYPE.UINT64;
+                _type = ELEMENTTYPE.SCALAR;
+                _componentType = DATATYPE.UINT64;
+                if (noData != null) _noData = noData;
+                if (defaultValue != null) _default = defaultValue;
                 return this;
             }
 
-            public StructuralMetadataClassProperty WithInt64Type()
+            public StructuralMetadataClassProperty WithInt64Type(long? noData = null, long? defaultValue = null)
             {
-                Type = ELEMENTTYPE.SCALAR;
-                ComponentType = DATATYPE.INT64;
+                _type = ELEMENTTYPE.SCALAR;
+                _componentType = DATATYPE.INT64;
+                if (noData != null) _noData = noData;
+                if (defaultValue != null) _default = defaultValue;
                 return this;
             }
 
-            public StructuralMetadataClassProperty WithFloat32Type()
+            public StructuralMetadataClassProperty WithFloat32Type(float? noData = null, float? defaultValue = null)
             {
-                Type = ELEMENTTYPE.SCALAR;
-                ComponentType = DATATYPE.FLOAT32;
+                _type = ELEMENTTYPE.SCALAR;
+                _componentType = DATATYPE.FLOAT32;
+                if (noData != null) _noData = noData;
+                if (defaultValue != null) _default = defaultValue;
                 return this;
             }
 
-            public StructuralMetadataClassProperty WithFloat64Type()
+            public StructuralMetadataClassProperty WithFloat64Type(double? noData = null, double? defaultValue = null)
             {
-                Type = ELEMENTTYPE.SCALAR;
-                ComponentType = DATATYPE.FLOAT64;
+                _type = ELEMENTTYPE.SCALAR;
+                _componentType = DATATYPE.FLOAT64;
+                if (noData != null) _noData = noData;
+                if (defaultValue != null) _default = defaultValue;
                 return this;
             }
 
 
-            public StructuralMetadataClassProperty WithVector3Type()
+            public StructuralMetadataClassProperty WithVector3Type(Vector3? noData = null, Vector3? defaultValue = null)
             {
-                Type = ElementType.VEC3;
-                ComponentType = DataType.FLOAT32;
+                _type = ElementType.VEC3;
+                _componentType = DataType.FLOAT32;
+
+                if (noData != null)
+                {
+                    _noData = new JsonArray(noData.Value.X, noData.Value.Y, noData.Value.Z);
+                }
+                if (defaultValue != null)
+                {
+                    _default = new JsonArray(defaultValue.Value.X, defaultValue.Value.Y, defaultValue.Value.Z);
+                }
+
                 return this;
             }
 
-            public StructuralMetadataClassProperty WithMatrix4x4Type()
+            public StructuralMetadataClassProperty WithMatrix4x4Type(Matrix4x4? noData = null, Matrix4x4? defaultValue = null)
             {
-                Type = ElementType.MAT4;
-                ComponentType = DataType.FLOAT32;
+                _type = ElementType.MAT4;
+                _componentType = DataType.FLOAT32;
+
+                if (noData != null)
+                {
+                    _noData = ToJsonArray(noData.Value);
+                }
+
+                if (defaultValue != null)
+                {
+                    _default = ToJsonArray(defaultValue.Value);
+                }
+
                 return this;
             }
 
-            public StructuralMetadataClassProperty WithCount()
+            public StructuralMetadataClassProperty WithBooleanArrayType(int? count = null)
             {
-                Type = ElementType.MAT4;
-                ComponentType = DataType.FLOAT32;
-                return this;
+                var property = WithArrayType(ELEMENTTYPE.BOOLEAN, null, count);
+                return property;
+            }
+
+            public StructuralMetadataClassProperty WithUInt8ArrayType(int? count = null, byte? noData = null)
+            {
+                var property = WithArrayType(ELEMENTTYPE.SCALAR, DATATYPE.UINT8, count);
+                if (noData != null) property._noData = noData;
+                return property;
+            }
+
+            public StructuralMetadataClassProperty WithInt8ArrayType(int? count = null, sbyte? noData = null)
+            {
+                var property = WithArrayType(ELEMENTTYPE.SCALAR, DATATYPE.INT8, count);
+                if (noData != null) property._noData = noData;
+                return property;
             }
 
+            public StructuralMetadataClassProperty WithInt16ArrayType(int? count = null, short? noData = null)
+            {
+                var property = WithArrayType(ELEMENTTYPE.SCALAR, DATATYPE.INT16, count);
+                if (noData != null) property._noData = noData;
+                return property;
+            }
 
-            //public StructuralMetadataClassProperty WithValueType(ELEMENTTYPE etype, DATATYPE? ctype = null)
-            //{
-            //    Type = etype;
-            //    ComponentType = ctype;
-            //    Array = false;
-            //    return this;
-            //}
+            public StructuralMetadataClassProperty WithUInt16ArrayType(int? count = null, ushort? noData = null)
+            {
+                var property = WithArrayType(ELEMENTTYPE.SCALAR, DATATYPE.UINT16, count);
+                if (noData != null) property._noData = noData;
+                return property;
+            }
+            public StructuralMetadataClassProperty WithInt32ArrayType(int? count = null, int? noData = null)
+            {
+                var property = WithArrayType(ELEMENTTYPE.SCALAR, DATATYPE.INT32, count);
+                if (noData != null) property._noData = noData;
+                return property;
+            }
+            public StructuralMetadataClassProperty WithUInt32ArrayType(int? count = null, uint? noData = null)
+            {
+                var property = WithArrayType(ELEMENTTYPE.SCALAR, DATATYPE.UINT32, count);
+                if (noData != null) property._noData = noData;
+                return property;
+            }
+            public StructuralMetadataClassProperty WithInt64ArrayType(int? count = null, long? noData = null)
+            {
+                var property = WithArrayType(ELEMENTTYPE.SCALAR, DATATYPE.INT64, count);
+                if (noData != null) property._noData = noData;
+                return property;
+            }
+            public StructuralMetadataClassProperty WithUInt64ArrayType(int? count = null, ulong? noData = null)
+            {
+                var property = WithArrayType(ELEMENTTYPE.SCALAR, DATATYPE.UINT64, count);
+                if (noData != null) property._noData = noData;
+                return property;
+            }
+            public StructuralMetadataClassProperty WithFloat32ArrayType(int? count = null, float? noData = null)
+            {
+                var property = WithArrayType(ELEMENTTYPE.SCALAR, DATATYPE.FLOAT32, count);
+                if (noData != null) property._noData = noData;
+                return property;
+            }
+            public StructuralMetadataClassProperty WithFloat64ArrayType(int? count = null, double? noData = null)
+            {
+                var property = WithArrayType(ELEMENTTYPE.SCALAR, DATATYPE.FLOAT64, count);
+                if (noData != null) property._noData = noData;
+                return property;
+            }
 
-            public StructuralMetadataClassProperty WithArrayType(ELEMENTTYPE etype, DATATYPE? ctype = null, int? count = null)
+            public StructuralMetadataClassProperty WithVector3ArrayType(int? count = null, Vector3? noData = null)
             {
-                Type = etype;
-                ComponentType = ctype;
-                Array = true;
-                Count = count;
-                return this;
+                var property = WithArrayType(ELEMENTTYPE.VEC3, DATATYPE.FLOAT32, count);
+                return property;
+            }
+            public StructuralMetadataClassProperty WithMatrix4x4ArrayType(int? count = null)
+            {
+                return WithArrayType(ELEMENTTYPE.MAT4, DATATYPE.FLOAT32, count);
             }
 
-            public StructuralMetadataClassProperty WithEnumArrayType(StructuralMetadataEnum enumeration, int? count = null)
+            public StructuralMetadataClassProperty WithStringArrayType(int? count = null)
             {
-                Type = ELEMENTTYPE.ENUM;
+                return WithArrayType(ELEMENTTYPE.STRING, null, count);
+            }
+
+            public StructuralMetadataClassProperty WithEnumArrayType(StructuralMetadataEnum enumeration, int? count = null, string noData = null)
+            {
+                _type = ELEMENTTYPE.ENUM;
                 _enumType = enumeration.LogicalKey;
-                Array = true;
-                Count = count;
+                _array = true;
+                _count = count;
+                if (noData != null) _noData = noData;
                 return this;
             }
 
-            public StructuralMetadataClassProperty WithEnumeration(StructuralMetadataEnum enumeration)
+            public StructuralMetadataClassProperty WithEnumeration(StructuralMetadataEnum enumeration, string noData = null)
             {
-                Type = ELEMENTTYPE.ENUM;
+                _type = ELEMENTTYPE.ENUM;
                 _enumType = enumeration.LogicalKey;
+                if (noData != null) _noData = noData;
                 return this;
             }
 
             public StructuralMetadataClassProperty WithRequired(bool required)
             {
-                Required = required;
+                _required = required;
                 return this;
             }
 
             public StructuralMetadataClassProperty WithNormalized(bool normalized)
             {
-                Normalized = normalized;
+                _normalized = normalized;
                 return this;
             }
 
+            private StructuralMetadataClassProperty WithArrayType(ELEMENTTYPE etype, DATATYPE? ctype = null, int? count = null)
+            {
+                _type = etype;
+                _componentType = ctype;
+                _array = true;
+                _count = count;
+                return this;
+            }
 
+            private static JsonArray ToJsonArray(Matrix4x4 m4)
+            {
+                return new JsonArray(
+                    m4.M11, m4.M12, m4.M13, m4.M14,
+                    m4.M21, m4.M22, m4.M23, m4.M24,
+                    m4.M31, m4.M32, m4.M33, m4.M34,
+                    m4.M41, m4.M42, m4.M43, m4.M44);
+            }
             #endregion
         }
 

+ 178 - 3
tests/SharpGLTF.Ext.3DTiles.Tests/ExtStructuralMetadataTests.cs

@@ -82,6 +82,181 @@ namespace SharpGLTF.Schema2.Tiles3D
             }
         }
 
+        /// <summary>
+        /// In this test a single triangle is defined, it has attributes defined for all types with a noData value, 
+        /// but the values are set to the noData value. In CesiumJS the triangle is rendered but the 
+        /// attritutes are not shown (because noData).
+        /// </summary>
+        [Test(Description = "MetadataAndNullValuesAttributeSample")]
+        public void MetadataNullValuesAttributeSample()
+        {
+            TestContext.CurrentContext.AttachGltfValidatorLinks();
+
+            int featureId = 0;
+            var material = MaterialBuilder.CreateDefault().WithDoubleSide(true);
+
+            var mesh = new MeshBuilder<VertexPositionNormal, VertexWithFeatureId, VertexEmpty>("mesh");
+            var prim = mesh.UsePrimitive(material);
+
+            var vt0 = VertexBuilder.GetVertexWithFeatureId(new Vector3(0, 0, 0), new Vector3(0, 0, 1), featureId);
+            var vt1 = VertexBuilder.GetVertexWithFeatureId(new Vector3(1, 0, 0), new Vector3(0, 0, 1), featureId);
+            var vt2 = VertexBuilder.GetVertexWithFeatureId(new Vector3(0, 1, 0), new Vector3(0, 0, 1), featureId);
+
+            prim.AddTriangle(vt0, vt1, vt2);
+            var scene = new SceneBuilder();
+            scene.AddRigidMesh(mesh, Matrix4x4.Identity);
+            var model = scene.ToGltf2();
+
+            var rootMetadata = model.UseStructuralMetadata();
+            var schema = rootMetadata.UseEmbeddedSchema("schema_001");
+
+            var schemaClass = schema.UseClassMetadata("triangles");
+
+            var speciesEnum = schema.UseEnumMetadata("speciesEnum", ("Unspecified", 0), ("Oak", 1), ("Pine", 2), ("Maple", 3));
+            speciesEnum.Name = "Species";
+            speciesEnum.Description = "An example enum for tree species.";
+
+            var descriptionProperty = schemaClass
+                    .UseProperty("description")
+                    .WithStringType();
+
+            // for this property, the default value (byte.MaxValue) should be shown in the client when the actual value is 
+            // equal to the noData value (byte.MinValue)
+            var uint8Property = schemaClass
+                .UseProperty("uint8")
+                .WithUInt8Type(byte.MinValue, byte.MaxValue);
+
+            var int8Property = schemaClass
+                .UseProperty("int8")
+                .WithInt8Type(sbyte.MinValue);
+
+            var int16Property = schemaClass
+                .UseProperty("int16")
+                .WithInt16Type(short.MinValue);
+
+            var uint16Property = schemaClass
+                .UseProperty("uint16")
+                .WithUInt16Type(ushort.MinValue);
+
+            var int32Property = schemaClass
+                .UseProperty("int32")
+                .WithInt32Type(int.MinValue);
+
+            var uint32Property = schemaClass
+                .UseProperty("uint32")
+                .WithUInt32Type(uint.MinValue);
+
+            var int64Property = schemaClass
+                .UseProperty("int64")
+                .WithInt64Type(long.MinValue);
+
+            var uint64Property = schemaClass
+                .UseProperty("uint64")
+                .WithUInt64Type(ulong.MinValue);
+
+            // when using float.MinValue there is an error in the validator: ""The value has type FLOAT32 and must be in [-3.4028234663852886e+38,3.4028234663852886e+38], but is -3.4028235e+38"
+            // And the noData value is shown in CesiumJS. Therefore we use -10.0f here.
+            var float32Property = schemaClass
+                .UseProperty("float32")
+                .WithFloat32Type(-10.0f);
+
+            var float64Property = schemaClass
+                .UseProperty("float64")
+                .WithFloat64Type(double.MinValue);
+
+            var stringProperty = schemaClass
+                .UseProperty("string")
+                .WithStringType("noData", "-");
+
+            var speciesProperty = schemaClass
+                .UseProperty("species")
+                .WithDescription("Type of tree.")
+                .WithEnumeration(speciesEnum, "Unspecified")
+                .WithRequired(false);
+
+            var vector3Property = schemaClass
+                .UseProperty("vector3")
+                .WithVector3Type(new Vector3(-10.0f, -10.0f, -10.0f));
+
+            var matrix4x4Property = schemaClass
+                .UseProperty("matrix4x4")
+                .WithMatrix4x4Type(Matrix4x4.Identity * -10);
+
+            var propertyTable = schemaClass.AddPropertyTable(1);
+
+            propertyTable
+                .UseProperty(descriptionProperty)
+                .SetValues("Description of the triangle");
+
+            propertyTable
+                .UseProperty(uint8Property)
+                .SetValues(byte.MinValue);
+
+            propertyTable
+                .UseProperty(int8Property)
+                .SetValues(sbyte.MinValue);
+
+            propertyTable
+                .UseProperty(int16Property)
+                .SetValues(short.MinValue);
+
+            propertyTable
+                .UseProperty(uint16Property)
+                .SetValues(ushort.MinValue);
+
+            propertyTable
+                .UseProperty(int32Property)
+                .SetValues(int.MinValue);
+
+            propertyTable
+                .UseProperty(uint32Property)
+                .SetValues(uint.MinValue);
+
+            propertyTable
+                .UseProperty(int64Property)
+                .SetValues(long.MinValue);
+
+            propertyTable
+                .UseProperty(uint64Property)
+                .SetValues(ulong.MinValue);
+
+            propertyTable
+                .UseProperty(float32Property)
+                .SetValues(-10f);
+
+            propertyTable
+                .UseProperty(float64Property)
+                .SetValues(double.MinValue);
+
+            propertyTable
+                .UseProperty(stringProperty)
+                .SetValues("noData");
+
+            propertyTable
+                .UseProperty(speciesProperty)
+                .SetValues((short)0);
+
+            propertyTable
+                .UseProperty(vector3Property)
+                .SetValues(new Vector3(10.0f,10.0f,10.0f));
+
+            var m4 = Matrix4x4.Identity;
+            propertyTable
+                .UseProperty(matrix4x4Property)
+                .SetValues(m4);
+
+            foreach (var primitive in model.LogicalMeshes[0].Primitives)
+            {
+                var featureIdAttribute = new FeatureIDBuilder(1, 0, propertyTable);
+                primitive.AddMeshFeatureIds(featureIdAttribute);
+            }
+
+            // create files
+            var ctx = new ValidationResult(model, ValidationMode.Strict, true);
+            model.AttachToCurrentTest("cesium_ext_structural_minimal_metadata_sample.glb");
+            model.AttachToCurrentTest("cesium_ext_structural_minimal_metadata_sample.gltf");
+            model.AttachToCurrentTest("cesium_ext_structural_minimal_metadata_sample.plotly");
+        }
 
         [Test(Description = "MinimalMetadataAttributeSample")]
         public void MinimalMetadataAttributeSample()
@@ -681,21 +856,21 @@ namespace SharpGLTF.Schema2.Tiles3D
                 .UseProperty("example_variable_length_ARRAY_normalized_UINT8")
                 .WithName("Example variable-length ARRAY normalized INT8 property")
                 .WithDescription("An example property, with type ARRAY, with component type UINT8, normalized, and variable length")
-                .WithArrayType(ElementType.SCALAR, DataType.UINT8)
+                .WithUInt8ArrayType()
                 .WithNormalized(false);
 
             var fixedLengthBooleanProperty = exampleMetadataClass
                 .UseProperty("example_fixed_length_ARRAY_BOOLEAN")
                 .WithName("Example fixed-length ARRAY BOOLEAN property")
                 .WithDescription("An example property, with type ARRAY, with component type BOOLEAN, and fixed length ")
-                .WithArrayType(ElementType.BOOLEAN, null, 4)
+                .WithBooleanArrayType(4)
                 .WithNormalized(false);
 
             var variableLengthStringArrayProperty = exampleMetadataClass
                 .UseProperty("example_variable_length_ARRAY_STRING")
                 .WithName("Example variable-length ARRAY STRING property")
                 .WithDescription("An example property, with type ARRAY, with component type STRING, and variable length")
-                .WithArrayType(ElementType.STRING);
+                .WithStringArrayType();
 
             var fixed_length_ARRAY_ENUM = exampleMetadataClass
                 .UseProperty("example_fixed_length_ARRAY_ENUM")