diff --git a/src/SharpGLTF.Ext.3DTiles/Schema2/Ext.StructuralMetadataRoot.cs b/src/SharpGLTF.Ext.3DTiles/Schema2/Ext.StructuralMetadataRoot.cs index c2a0d479..8f09b5b4 100644 --- a/src/SharpGLTF.Ext.3DTiles/Schema2/Ext.StructuralMetadataRoot.cs +++ b/src/SharpGLTF.Ext.3DTiles/Schema2/Ext.StructuralMetadataRoot.cs @@ -233,7 +233,6 @@ protected override void OnValidateContent(ValidationContext result) 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 @@ protected override void OnValidateContent(ValidationContext result) { 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'"); + } } } } @@ -843,13 +853,13 @@ private void CheckElementTypes(StructuralMetadataClassProperty metadataProper private void CheckScalarTypes(DATATYPE? componentType) { - if (componentType == DATATYPE.INT8) + if (componentType == DATATYPE.UINT8) { - Guard.IsTrue(typeof(T) == typeof(sbyte), nameof(T), $"Scalar value type of property {LogicalKey} must be sbyte"); + Guard.IsTrue(typeof(T) == typeof(byte), nameof(T), $"Scalar value type of property {LogicalKey} must be byte"); } - else if (componentType == DATATYPE.UINT8) + else if (componentType == DATATYPE.INT8) { - Guard.IsTrue(typeof(T) == typeof(byte), nameof(T), $"Scalar value type of property {LogicalKey} must be byte"); + Guard.IsTrue(typeof(T) == typeof(sbyte), nameof(T), $"Scalar value type of property {LogicalKey} must be sbyte"); } else if (componentType == DATATYPE.INT16) { @@ -1227,6 +1237,7 @@ void IChildOfDictionary.SetLogicalParent(StructuralMeta #endregion #region properties + public string Name { get => _name; @@ -1263,6 +1274,11 @@ public bool Required set => _required = value.AsNullable(_requiredDefault); } + public JsonNode NoData + { + get => _noData; + } + public bool Normalized { get => _normalized ?? _normalizedDefault; @@ -1325,9 +1341,10 @@ public StructuralMetadataClassProperty WithDescription(string description) return this; } - public StructuralMetadataClassProperty WithStringType() + public StructuralMetadataClassProperty WithStringType(string noData = null) { Type = ElementType.STRING; + if (noData != null) _noData = noData; return this; } @@ -1337,73 +1354,83 @@ public StructuralMetadataClassProperty WithBooleanType() return this; } - public StructuralMetadataClassProperty WithUInt8Type() + public StructuralMetadataClassProperty WithUInt8Type(byte? noData = null) { Type = ELEMENTTYPE.SCALAR; ComponentType = DATATYPE.UINT8; + if (noData != null) _noData = noData; return this; } - public StructuralMetadataClassProperty WithInt8Type() + public StructuralMetadataClassProperty WithInt8Type(sbyte? noData = null) { Type = ELEMENTTYPE.SCALAR; ComponentType = DATATYPE.INT8; + if (noData != null) _noData = noData; return this; } - public StructuralMetadataClassProperty WithUInt16Type() + public StructuralMetadataClassProperty WithUInt16Type(ushort? noData = null) { Type = ELEMENTTYPE.SCALAR; ComponentType = DATATYPE.UINT16; + if (noData != null) _noData = noData; return this; } - public StructuralMetadataClassProperty WithInt16Type() + public StructuralMetadataClassProperty WithInt16Type(short? noData = null) { Type = ELEMENTTYPE.SCALAR; ComponentType = DATATYPE.INT16; + if (noData != null) _noData = noData; return this; } - public StructuralMetadataClassProperty WithUInt32Type() + public StructuralMetadataClassProperty WithUInt32Type(uint? noData = null) { Type = ELEMENTTYPE.SCALAR; ComponentType = DATATYPE.UINT32; + if (noData != null) _noData = noData; return this; } - public StructuralMetadataClassProperty WithInt32Type() + public StructuralMetadataClassProperty WithInt32Type(int? noData = null) { Type = ELEMENTTYPE.SCALAR; ComponentType = DATATYPE.INT32; + if (noData != null) _noData = noData; return this; } - public StructuralMetadataClassProperty WithUInt64Type() + public StructuralMetadataClassProperty WithUInt64Type(ulong? noData = null) { Type = ELEMENTTYPE.SCALAR; ComponentType = DATATYPE.UINT64; + if (noData != null) _noData = noData; return this; } - public StructuralMetadataClassProperty WithInt64Type() + public StructuralMetadataClassProperty WithInt64Type(long? noData = null) { Type = ELEMENTTYPE.SCALAR; ComponentType = DATATYPE.INT64; + if (noData != null) _noData = noData; return this; } - public StructuralMetadataClassProperty WithFloat32Type() + public StructuralMetadataClassProperty WithFloat32Type(float? noData = null) { Type = ELEMENTTYPE.SCALAR; ComponentType = DATATYPE.FLOAT32; + if (noData != null) _noData = noData; return this; } - public StructuralMetadataClassProperty WithFloat64Type() + public StructuralMetadataClassProperty WithFloat64Type(double? noData = null) { Type = ELEMENTTYPE.SCALAR; ComponentType = DATATYPE.FLOAT64; + if (noData != null) _noData = noData; return this; } @@ -1429,16 +1456,95 @@ public StructuralMetadataClassProperty WithCount() return this; } + public StructuralMetadataClassProperty WithBooleanArrayType(int? count = null) + { + 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 WithValueType(ELEMENTTYPE etype, DATATYPE? ctype = null) - //{ - // Type = etype; - // ComponentType = ctype; - // Array = false; - // return this; - //} + 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 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 WithVector3ArrayType(int? count = null, Vector3? noData = null) + { + var property = WithArrayType(ELEMENTTYPE.VEC3, DATATYPE.FLOAT32, count); + // todo: how to set noData for vector3? + return property; + } + public StructuralMetadataClassProperty WithMatrix4x4ArrayType(int? count = null) + { + return WithArrayType(ELEMENTTYPE.MAT4, DATATYPE.FLOAT32, count); + } + + public StructuralMetadataClassProperty WithStringArrayType(int? count = null, string noData = null) + { + var property = WithArrayType(ELEMENTTYPE.STRING, null, count); + if (noData != null) property._noData = noData; + return property; + } - public StructuralMetadataClassProperty WithArrayType(ELEMENTTYPE etype, DATATYPE? ctype = null, int? count = null) + private StructuralMetadataClassProperty WithArrayType(ELEMENTTYPE etype, DATATYPE? ctype = null, int? count = null) { Type = etype; ComponentType = ctype; @@ -1447,19 +1553,21 @@ public StructuralMetadataClassProperty WithArrayType(ELEMENTTYPE etype, DATATYPE return this; } - public StructuralMetadataClassProperty WithEnumArrayType(StructuralMetadataEnum enumeration, int? count = null) + public StructuralMetadataClassProperty WithEnumArrayType(StructuralMetadataEnum enumeration, int? count = null, string noData = null) { Type = ELEMENTTYPE.ENUM; _enumType = enumeration.LogicalKey; 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; _enumType = enumeration.LogicalKey; + if (noData != null) _noData = noData; return this; } diff --git a/tests/SharpGLTF.Ext.3DTiles.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Ext.3DTiles.Tests/ExtStructuralMetadataTests.cs index 99d243b7..1b51eac7 100644 --- a/tests/SharpGLTF.Ext.3DTiles.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Ext.3DTiles.Tests/ExtStructuralMetadataTests.cs @@ -82,6 +82,176 @@ public void ReadExtStructuralMetadata(string file, Type exception = null) } } + /// + /// 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). + /// + [Test(Description = "MetadataAndNullValuesAttributeSample")] + public void MetadataNullValuesAttributeSample() + { + TestContext.CurrentContext.AttachGltfValidatorLinks(); + + int featureId = 0; + var material = MaterialBuilder.CreateDefault().WithDoubleSide(true); + + var mesh = new MeshBuilder("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(); + + var uint8Property = schemaClass + .UseProperty("uint8") + .WithUInt8Type(byte.MinValue); + + 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 vector3Property = schemaClass + .UseProperty("vector3") + .WithVector3Type(); + + var stringProperty = schemaClass + .UseProperty("string") + .WithStringType("noData"); + + var speciesProperty = schemaClass + .UseProperty("species") + .WithDescription("Type of tree.") + .WithEnumeration(speciesEnum, "Unspecified") + .WithRequired(false); + + // todo add array types + + 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); + + // todo: how to set a vector3 to null? + propertyTable + .UseProperty(vector3Property) + .SetValues(new Vector3(0,0,0)); + + propertyTable + .UseProperty(stringProperty) + .SetValues("noData"); + + propertyTable + .UseProperty(speciesProperty) + .SetValues((short)0); + + 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 +851,21 @@ public void ComplexTypesTest() .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")