diff --git a/CHANGES.md b/CHANGES.md index e47fa63596c..194df3ecc3a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,10 @@ - Fix flickering issue caused by bounding sphere retrieval being blocked by the bounding sphere of another entity. [#12230](https://github.com/CesiumGS/cesium/pull/12230) +##### Fixes :wrench: + +- Properly handle `offset` and `scale` properties when picking metadata from property textures. [#12237](https://github.com/CesiumGS/cesium/pull/12237) + ### 1.122 - 2024-10-01 #### @cesium/engine diff --git a/packages/engine/Source/Scene/DerivedCommand.js b/packages/engine/Source/Scene/DerivedCommand.js index 8d70e283ac6..8e3478fc65f 100644 --- a/packages/engine/Source/Scene/DerivedCommand.js +++ b/packages/engine/Source/Scene/DerivedCommand.js @@ -1,3 +1,4 @@ +import { MetadataComponentType } from "@cesium/engine"; import defined from "../Core/defined.js"; import DrawCommand from "../Renderer/DrawCommand.js"; import RenderState from "../Renderer/RenderState.js"; @@ -416,6 +417,98 @@ function getGlslType(classProperty) { return `ivec${componentCount}`; } +/** + * Returns a shader statement that applies the inverse of the + * value transform to the given value, based on the given offset + * and scale. + * + * @param {string} input The input value + * @param {string} offset The offset + * @param {string} scale The scale + * @returns {string} The statement + */ +function unapplyValueTransform(input, offset, scale) { + return `((${input} - float(${offset})) / float(${scale}))`; +} + +/** + * Returns a shader statement that applies the inverse of the + * normalization, based on the given component type + * + * @param {string} input The input value + * @param {string} componentType The component type + * @returns {string} The statement + */ +function unnormalize(input, componentType) { + const max = MetadataComponentType.getMaximum(componentType); + return `(${input}) / float(${max})`; +} + +/** + * Creates a shader statement that returns the value of the specified + * property, normalized to the range [0, 1]. + * + * @param {MetadataClassProperty} classProperty The class property + * @param {object} metadataProperty The metadata property, either + * a `PropertyTextureProperty` or a `PropertyAttributeProperty` + * @returns {string} The string + */ +function getSourceValueStringScalar(classProperty, metadataProperty) { + let result = `float(value)`; + + // The 'hasValueTransform' indicates whether the property + // (or its class property) did define an 'offset' or 'scale'. + // Even when they had not been defined in the JSON, they are + // defined in the object, with default values. + if (metadataProperty.hasValueTransform) { + const offset = metadataProperty.offset; + const scale = metadataProperty.scale; + result = unapplyValueTransform(result, offset, scale); + } + if (!classProperty.normalized) { + result = unnormalize(result, classProperty.componentType); + } + return result; +} + +/** + * Creates a shader statement that returns the value of the specified + * component of the given property, normalized to the range [0, 1]. + * + * @param {MetadataClassProperty} classProperty The class property + * @param {object} metadataProperty The metadata property, either + * a `PropertyTextureProperty` or a `PropertyAttributeProperty` + * @param {string} componentName The name, in ["x", "y", "z", "w"] + * @returns {string} The string + */ +function getSourceValueStringComponent( + classProperty, + metadataProperty, + componentName, +) { + const valueString = `value.${componentName}`; + let result = `float(${valueString})`; + + // The 'hasValueTransform' indicates whether the property + // (or its class property) did define an 'offset' or 'scale'. + // Even when they had not been defined in the JSON, they are + // defined in the object, with default values + // Note that in the 'PropertyTextureProperty' and the + // 'PropertyAttributeProperty', these values are + // stored as "object types" (like 'Cartesian2'), whereas + // in the 'MetadataClassProperty', they are stored as + // "array types", e.g. a `[number, number]` + if (metadataProperty.hasValueTransform) { + const offset = metadataProperty.offset[componentName]; + const scale = metadataProperty.scale[componentName]; + result = unapplyValueTransform(result, offset, scale); + } + if (!classProperty.normalized) { + result = unnormalize(result, classProperty.componentType); + } + return result; +} + /** * Creates a new `ShaderProgram` from the given input that renders metadata * values into the frame buffer, according to the given picked metadata info. @@ -452,34 +545,34 @@ function getPickMetadataShaderProgram( return shader; } + const metadataProperty = pickedMetadataInfo.metadataProperty; const classProperty = pickedMetadataInfo.classProperty; const glslType = getGlslType(classProperty); // Define the components that will go into the output `metadataValues`. + // This will be the 'color' that is written into the frame buffer, + // meaning that the values should be in [0.0, 1.0], and will become + // values in [0, 255] in the frame buffer. // By default, all of them are 0.0. const sourceValueStrings = ["0.0", "0.0", "0.0", "0.0"]; const componentCount = getComponentCount(classProperty); if (componentCount === 1) { - // When the property is a scalar, store its value directly - // in `metadataValues.x` - sourceValueStrings[0] = `float(value)`; + // When the property is a scalar, store the source value + // string directly in `metadataValues.x` + sourceValueStrings[0] = getSourceValueStringScalar( + classProperty, + metadataProperty, + ); } else { // When the property is an array, store the array elements // in `metadataValues.x/y/z/w` - const components = ["x", "y", "z", "w"]; - for (let i = 0; i < componentCount; i++) { - const component = components[i]; - const valueString = `value.${component}`; - sourceValueStrings[i] = `float(${valueString})`; - } - } - - // Make sure that the `metadataValues` components are all in - // the range [0, 1] (which will result in RGBA components - // in [0, 255] during rendering) - if (!classProperty.normalized) { + const componentNames = ["x", "y", "z", "w"]; for (let i = 0; i < componentCount; i++) { - sourceValueStrings[i] += " / 255.0"; + sourceValueStrings[i] = getSourceValueStringComponent( + classProperty, + metadataProperty, + componentNames[i], + ); } } diff --git a/packages/engine/Source/Scene/MetadataClassProperty.js b/packages/engine/Source/Scene/MetadataClassProperty.js index dec9b1393e3..b51a7988481 100644 --- a/packages/engine/Source/Scene/MetadataClassProperty.js +++ b/packages/engine/Source/Scene/MetadataClassProperty.js @@ -438,6 +438,10 @@ Object.defineProperties(MetadataClassProperty.prototype, { /** * The offset to be added to property values as part of the value transform. * + * This is always defined, even when `hasValueTransform` is `false`. If + * the class property JSON itself did not define it, then it will be + * initialized to the default value. + * * @memberof MetadataClassProperty.prototype * @type {number|number[]|number[][]} * @readonly @@ -451,6 +455,10 @@ Object.defineProperties(MetadataClassProperty.prototype, { /** * The scale to be multiplied to property values as part of the value transform. * + * This is always defined, even when `hasValueTransform` is `false`. If + * the class property JSON itself did not define it, then it will be + * initialized to the default value. + * * @memberof MetadataClassProperty.prototype * @type {number|number[]|number[][]} * @readonly @@ -1139,6 +1147,24 @@ function normalizeInPlace(values, valueType, normalizeFunction) { } /** + * Applies the value transform that is defined with the given offsets + * and scales to the given values. + * + * If the given values are not an array, then the given transformation + * function will be applied directly. + * + * If the values are an array, then this function will be called recursively + * with the array elements, boiling down to a component-wise application + * of the transformation function to the innermost array elements. + * + * @param {number|number[]|number[][]} values The input values + * @param {number|number[]|number[][]} offsets The offsets + * @param {number|number[]|number[][]} scales The scales + * @param {Function} transformationFunction The function with the signature + * `(value:number, offset:number, scale:number) : number` that will be + * applied to the innermost elements + * @returns The input values (or the result of applying the transformation + * function to a single value if the values have not been an array). * @private */ MetadataClassProperty.valueTransformInPlace = function ( diff --git a/packages/engine/Source/Scene/MetadataPicking.js b/packages/engine/Source/Scene/MetadataPicking.js index 87f33a91d7d..5364824c64b 100644 --- a/packages/engine/Source/Scene/MetadataPicking.js +++ b/packages/engine/Source/Scene/MetadataPicking.js @@ -6,6 +6,7 @@ import Matrix2 from "../Core/Matrix2.js"; import Matrix3 from "../Core/Matrix3.js"; import Matrix4 from "../Core/Matrix4.js"; import RuntimeError from "../Core/RuntimeError.js"; +import MetadataClassProperty from "./MetadataClassProperty.js"; import MetadataComponentType from "./MetadataComponentType.js"; import MetadataType from "./MetadataType.js"; @@ -29,6 +30,10 @@ const MetadataPicking = {}; * @param {DataView} dataView The data view * @param {number} index The index (byte offset) * @returns {number|bigint|undefined} The value + * @throws RuntimeError If the given component type is not a valid + * `MetadataComponentType` + * @throws RangeError If reading the data from the given data view would + * cause an out-of-bounds access * * @private */ @@ -43,21 +48,21 @@ MetadataPicking.decodeRawMetadataValue = function ( case MetadataComponentType.UINT8: return dataView.getUint8(index); case MetadataComponentType.INT16: - return dataView.getInt16(index); + return dataView.getInt16(index, true); case MetadataComponentType.UINT16: - return dataView.getUint16(index); + return dataView.getUint16(index, true); case MetadataComponentType.INT32: - return dataView.getInt32(index); + return dataView.getInt32(index, true); case MetadataComponentType.UINT32: - return dataView.getUint32(index); + return dataView.getUint32(index, true); case MetadataComponentType.INT64: - return dataView.getBigInt64(index); + return dataView.getBigInt64(index, true); case MetadataComponentType.UINT64: - return dataView.getBigUint64(index); + return dataView.getBigUint64(index, true); case MetadataComponentType.FLOAT32: - return dataView.getFloat32(index); + return dataView.getFloat32(index, true); case MetadataComponentType.FLOAT64: - return dataView.getFloat64(index); + return dataView.getFloat64(index, true); } throw new RuntimeError(`Invalid component type: ${componentType}`); }; @@ -77,6 +82,10 @@ MetadataPicking.decodeRawMetadataValue = function ( * @param {number} dataViewOffset The byte offset within the data view from * which the component should be read * @returns {number|bigint|undefined} The metadata value component + * @throws RuntimeError If the component of the given property is not + * a valid `MetadataComponentType` + * @throws RangeError If reading the data from the given data view would + * cause an out-of-bounds access */ MetadataPicking.decodeRawMetadataValueComponent = function ( classProperty, @@ -114,6 +123,11 @@ MetadataPicking.decodeRawMetadataValueComponent = function ( * @param {number} elementIndex The index of the element. This is the index * inside the array for array-typed properties, and 0 for non-array types. * @returns {number|number[]|bigint|bigint[]|undefined} The decoded metadata value element + * @throws RuntimeError If the component of the given property is not + * a valid `MetadataComponentType` + * @throws RangeError If reading the data from the given data view would + * cause an out-of-bounds access + * */ MetadataPicking.decodeRawMetadataValueElement = function ( classProperty, @@ -183,6 +197,7 @@ MetadataPicking.decodeRawMetadataValueElement = function ( * @param {MetadataClassProperty} classProperty The `MetadataClassProperty` * @param {Uint8Array} rawPixelValues The raw values * @returns {number|bigint|number[]|bigint[]|undefined} The value + * @throws RuntimeError If the class property has an invalid component type * * @private */ @@ -230,6 +245,7 @@ MetadataPicking.decodeRawMetadataValues = function ( * @param {string} type The `ClassProperty` type * @param {number|bigint|number[]|bigint[]|undefined} value The input value * @returns {any} The object representation + * @throws RuntimeError If the type is not a valid `MetadataType` */ MetadataPicking.convertToObjectType = function (type, value) { if (!defined(value)) { @@ -250,7 +266,7 @@ MetadataPicking.convertToObjectType = function (type, value) { case MetadataType.VEC3: return Cartesian3.unpack(numbers, 0, new Cartesian3()); case MetadataType.VEC4: - return Cartesian4.unpack(numbers, 0, new Cartesian3()); + return Cartesian4.unpack(numbers, 0, new Cartesian4()); case MetadataType.MAT2: return Matrix2.unpack(numbers, 0, new Matrix2()); case MetadataType.MAT3: @@ -259,29 +275,96 @@ MetadataPicking.convertToObjectType = function (type, value) { return Matrix4.unpack(numbers, 0, new Matrix4()); } // Should never happen: - return value; + throw new RuntimeError(`Invalid metadata object type: ${type}`); +}; + +/** + * Converts the given type into a raw value or array representation. + * + * For `VECn/MATn` types, the given value is converted into an array. + * For other types, the value is returned directly + * + * @param {string} type The `ClassProperty` type + * @param {any} value The input value + * @returns {any} The array representation + * @throws RuntimeError If the type is not a valid `MetadataType` + */ +MetadataPicking.convertFromObjectType = function (type, value) { + if (!defined(value)) { + return value; + } + if ( + type === MetadataType.SCALAR || + type === MetadataType.STRING || + type === MetadataType.BOOLEAN || + type === MetadataType.ENUM + ) { + return value; + } + switch (type) { + case MetadataType.VEC2: + return Cartesian2.pack(value, Array(2)); + case MetadataType.VEC3: + return Cartesian3.pack(value, Array(3)); + case MetadataType.VEC4: + return Cartesian4.pack(value, Array(4)); + case MetadataType.MAT2: + return Matrix2.pack(value, Array(4)); + case MetadataType.MAT3: + return Matrix3.pack(value, Array(9)); + case MetadataType.MAT4: + return Matrix4.pack(value, Array(16)); + } + // Should never happen: + throw new RuntimeError(`Invalid metadata object type: ${type}`); }; /** * Decode the given raw values into a metadata property value. * - * This just converts the result of `decodeRawMetadataValues` - * from array-based types into object types like `CartesianN`. + * This applies the value transform (offset/scale) to the result + * of `decodeRawMetadataValues`, and converts this from array-based + * types into object types like `CartesianN`. * * @param {MetadataClassProperty} classProperty The `MetadataClassProperty` + * @param {MetadataClassProperty} metadataProperty The + * `PropertyTextureProperty` or `PropertyAttributeProperty` * @param {Uint8Array} rawPixelValues The raw values * @returns {any} The value + * @throws RuntimeError If the class property has an invalid type + * or component type + * @throws RangeError If the given pixel values do not have sufficient + * size to contain the expected value type * * @private */ MetadataPicking.decodeMetadataValues = function ( classProperty, + metadataProperty, rawPixelValues, ) { - const arrayBasedResult = MetadataPicking.decodeRawMetadataValues( + let arrayBasedResult = MetadataPicking.decodeRawMetadataValues( classProperty, rawPixelValues, ); + + if (metadataProperty.hasValueTransform) { + const offset = MetadataPicking.convertFromObjectType( + classProperty.type, + metadataProperty.offset, + ); + const scale = MetadataPicking.convertFromObjectType( + classProperty.type, + metadataProperty.scale, + ); + arrayBasedResult = MetadataClassProperty.valueTransformInPlace( + arrayBasedResult, + offset, + scale, + MetadataComponentType.applyValueTransform, + ); + } + if (classProperty.isArray) { const arrayLength = classProperty.arrayLength; const result = Array(arrayLength); @@ -295,11 +378,11 @@ MetadataPicking.decodeMetadataValues = function ( } return result; } - const result = MetadataPicking.convertToObjectType( + const objectResult = MetadataPicking.convertToObjectType( classProperty.type, arrayBasedResult, ); - return result; + return objectResult; }; export default Object.freeze(MetadataPicking); diff --git a/packages/engine/Source/Scene/Model/Extensions/Gpm/GltfMeshPrimitiveGpmLoader.js b/packages/engine/Source/Scene/Model/Extensions/Gpm/GltfMeshPrimitiveGpmLoader.js index 7be02214431..b3ef4f258ed 100644 --- a/packages/engine/Source/Scene/Model/Extensions/Gpm/GltfMeshPrimitiveGpmLoader.js +++ b/packages/engine/Source/Scene/Model/Extensions/Gpm/GltfMeshPrimitiveGpmLoader.js @@ -268,9 +268,9 @@ GltfMeshPrimitiveGpmLoader._createPpeTextureClassJson = function ( // property values when they are `normalized`, the values will be // declared as `normalized` here. // The normalization factor will later have to be cancelled out, - // when integrating the `scale` into the actual property texture - // property. In the property texture property, the `scale` has to - // be multiplied by 255. + // with the `scale` being multiplied by 255. + const offset = ppeTexture.offset ?? 0.0; + const scale = (ppeTexture.scale ?? 1.0) * 255.0; const classJson = { name: `PPE texture class ${index}`, properties: { @@ -279,6 +279,8 @@ GltfMeshPrimitiveGpmLoader._createPpeTextureClassJson = function ( type: "SCALAR", componentType: "UINT8", normalized: true, + offset: offset, + scale: scale, min: traits.min, max: traits.max, }, @@ -406,19 +408,12 @@ GltfMeshPrimitiveGpmLoader._convertToStructuralMetadata = function ( const ppePropertyName = traits.source; const metadataClass = ppeTexturesMetadataSchema.classes[classId]; - // The class property has been declared as `normalized`, so - // that `offset` and `scale` can be applied. The normalization - // factor has to be cancelled out here, by multiplying the - // `scale` with 255. - const scale = (ppeTexture.scale ?? 1.0) * 255.0; const ppeTextureAsPropertyTexture = { class: classId, properties: { [ppePropertyName]: { index: ppeTexture.index, texCoord: ppeTexture.texCoord, - offset: ppeTexture.offset, - scale: scale, }, }, }; diff --git a/packages/engine/Source/Scene/PickedMetadataInfo.js b/packages/engine/Source/Scene/PickedMetadataInfo.js index 1e8de1902b1..40a727a8de4 100644 --- a/packages/engine/Source/Scene/PickedMetadataInfo.js +++ b/packages/engine/Source/Scene/PickedMetadataInfo.js @@ -6,11 +6,17 @@ * the metadata values of an object into the picking frame buffer. The * raw values are read from that buffer, and are then translated back into * proper metadata values in `Picking.pickMetadata`, using the structural - * information about the metadata `classProperty` that is stored here. + * information about the metadata that is stored here. * * @private */ -function PickedMetadataInfo(schemaId, className, propertyName, classProperty) { +function PickedMetadataInfo( + schemaId, + className, + propertyName, + classProperty, + metadataProperty, +) { /** * The optional ID of the metadata schema * @@ -29,11 +35,22 @@ function PickedMetadataInfo(schemaId, className, propertyName, classProperty) { * @type {string} */ this.propertyName = propertyName; + /** - * The optional ID of the metadata schema + * The the `MetadataClassProperty` that is described by this + * structure, as obtained from the `MetadataSchema` * * @type {MetadataClassProperty} */ this.classProperty = classProperty; + + /** + * The metadata property that is described by this structure, as + * obtained from the property texture or property attribute of the + * `StructuralMetadata` that matches the class name and property name. + * + * @type {PropertyTextureProperty|PropertyAttributeProperty} + */ + this.metadataProperty = metadataProperty; } export default PickedMetadataInfo; diff --git a/packages/engine/Source/Scene/Picking.js b/packages/engine/Source/Scene/Picking.js index fc38a4a9095..8ded3ccbe40 100644 --- a/packages/engine/Source/Scene/Picking.js +++ b/packages/engine/Source/Scene/Picking.js @@ -517,6 +517,7 @@ Picking.prototype.pickMetadata = function ( const metadataValue = MetadataPicking.decodeMetadataValues( pickedMetadataInfo.classProperty, + pickedMetadataInfo.metadataProperty, rawMetadataPixel, ); diff --git a/packages/engine/Source/Scene/PropertyAttributeProperty.js b/packages/engine/Source/Scene/PropertyAttributeProperty.js index 607fb861147..70c04cfd1ab 100644 --- a/packages/engine/Source/Scene/PropertyAttributeProperty.js +++ b/packages/engine/Source/Scene/PropertyAttributeProperty.js @@ -80,7 +80,7 @@ Object.defineProperties(PropertyAttributeProperty.prototype, { * True if offset/scale should be applied. If both offset/scale were * undefined, they default to identity so this property is set false * - * @memberof MetadataClassProperty.prototype + * @memberof PropertyAttributeProperty.prototype * @type {boolean} * @readonly * @private @@ -94,7 +94,13 @@ Object.defineProperties(PropertyAttributeProperty.prototype, { /** * The offset to be added to property values as part of the value transform. * - * @memberof MetadataClassProperty.prototype + * This is always defined, even when `hasValueTransform` is `false`. If + * the property JSON itself did not define it, then it will inherit the + * value from the `MetadataClassProperty`. There, it also is always + * defined, and initialized to the default value if it was not contained + * in the class property JSON. + * + * @memberof PropertyAttributeProperty.prototype * @type {number|Cartesian2|Cartesian3|Cartesian4|Matrix2|Matrix3|Matrix4} * @readonly * @private @@ -108,7 +114,13 @@ Object.defineProperties(PropertyAttributeProperty.prototype, { /** * The scale to be multiplied to property values as part of the value transform. * - * @memberof MetadataClassProperty.prototype + * This is always defined, even when `hasValueTransform` is `false`. If + * the property JSON itself did not define it, then it will inherit the + * value from the `MetadataClassProperty`. There, it also is always + * defined, and initialized to the default value if it was not contained + * in the class property JSON. + * + * @memberof PropertyAttributeProperty.prototype * @type {number|Cartesian2|Cartesian3|Cartesian4|Matrix2|Matrix3|Matrix4} * @readonly * @private diff --git a/packages/engine/Source/Scene/PropertyTextureProperty.js b/packages/engine/Source/Scene/PropertyTextureProperty.js index 765c2507313..cbb7508cc11 100644 --- a/packages/engine/Source/Scene/PropertyTextureProperty.js +++ b/packages/engine/Source/Scene/PropertyTextureProperty.js @@ -111,6 +111,12 @@ Object.defineProperties(PropertyTextureProperty.prototype, { /** * The offset to be added to property values as part of the value transform. * + * This is always defined, even when `hasValueTransform` is `false`. If + * the property JSON itself did not define it, then it will inherit the + * value from the `MetadataClassProperty`. There, it also is always + * defined, and initialized to the default value if it was not contained + * in the class property JSON. + * * @memberof PropertyTextureProperty.prototype * @type {number|Cartesian2|Cartesian3|Cartesian4|Matrix2|Matrix3|Matrix4} * @readonly @@ -125,6 +131,12 @@ Object.defineProperties(PropertyTextureProperty.prototype, { /** * The scale to be multiplied to property values as part of the value transform. * + * This is always defined, even when `hasValueTransform` is `false`. If + * the property JSON itself did not define it, then it will inherit the + * value from the `MetadataClassProperty`. There, it also is always + * defined, and initialized to the default value if it was not contained + * in the class property JSON. + * * @memberof PropertyTextureProperty.prototype * @type {number|Cartesian2|Cartesian3|Cartesian4|Matrix2|Matrix3|Matrix4} * @readonly diff --git a/packages/engine/Source/Scene/Scene.js b/packages/engine/Source/Scene/Scene.js index e751dda9408..0a2543ef7fd 100644 --- a/packages/engine/Source/Scene/Scene.js +++ b/packages/engine/Source/Scene/Scene.js @@ -79,6 +79,7 @@ import VoxelCell from "./VoxelCell.js"; import VoxelPrimitive from "./VoxelPrimitive.js"; import getMetadataClassProperty from "./getMetadataClassProperty.js"; import PickedMetadataInfo from "./PickedMetadataInfo.js"; +import getMetadataProperty from "./getMetadataProperty.js"; const requestRenderAfterFrame = function (scene) { return function () { @@ -4402,7 +4403,7 @@ Scene.prototype.pickVoxel = function (windowPosition, width, height) { * values from * @param {string} propertyName The name of the metadata property to pick * values from - * @returns The metadata value + * @returns {any} The metadata value * * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. */ @@ -4426,7 +4427,11 @@ Scene.prototype.pickMetadata = function ( // Check if the picked object is a model that has structural // metadata, with a schema that contains the specified // property. - const schema = pickedObject.detail?.model?.structuralMetadata?.schema; + const structuralMetadata = pickedObject.detail?.model?.structuralMetadata; + if (!defined(structuralMetadata)) { + return undefined; + } + const schema = structuralMetadata.schema; const classProperty = getMetadataClassProperty( schema, schemaId, @@ -4436,12 +4441,21 @@ Scene.prototype.pickMetadata = function ( if (!defined(classProperty)) { return undefined; } + const metadataProperty = getMetadataProperty( + structuralMetadata, + className, + propertyName, + ); + if (!defined(metadataProperty)) { + return undefined; + } const pickedMetadataInfo = new PickedMetadataInfo( schemaId, className, propertyName, classProperty, + metadataProperty, ); const pickedMetadataValues = this._picking.pickMetadata( diff --git a/packages/engine/Source/Scene/getMetadataProperty.js b/packages/engine/Source/Scene/getMetadataProperty.js new file mode 100644 index 00000000000..291f4f63985 --- /dev/null +++ b/packages/engine/Source/Scene/getMetadataProperty.js @@ -0,0 +1,48 @@ +import defined from "../Core/defined.js"; + +/** + * Return the `PropertyTextureProperty` from the given `StructuralMetadata` + * that matches the given description. + * + * If the given structural metadata is `undefined`, then `undefined` is returned. + * + * Otherwise, this method will check all the property textures in the given + * structural metadata. + * + * If it finds a property texture that has a class with an `_id` that matches + * the given name, and that contains a property for the given property name, then + * this property is returned. + * + * Otherwise, `undefined` is returned + * + * @param {StructuralMetadata} structuralMetadata The structural metadata + * @param {string} className The name of the metadata class + * @param {string} propertyName The name of the metadata property + * @returns {PropertyTextureProperty|undefined} + * @private + */ +function getMetadataProperty(structuralMetadata, className, propertyName) { + if (!defined(structuralMetadata)) { + return undefined; + } + const propertyTextures = structuralMetadata.propertyTextures; + for (const propertyTexture of propertyTextures) { + const metadataClass = propertyTexture.class; + if (metadataClass.id === className) { + const properties = propertyTexture.properties; + const property = properties[propertyName]; + if (defined(property)) { + return property; + } + } + } + // Note: This could check for property attributes in a similar + // way. But since picking arbitrary property attributes via the + // frame buffer is not supported yet, returning "undefined" here + // causes the picking to bail out early and safely when no + // property texture was found. + // See https://github.com/CesiumGS/cesium/issues/12225 + return undefined; +} + +export default getMetadataProperty; diff --git a/packages/engine/Specs/Scene/MetadataPickingSpec.js b/packages/engine/Specs/Scene/MetadataPickingSpec.js new file mode 100644 index 00000000000..5f1cb8bd5a7 --- /dev/null +++ b/packages/engine/Specs/Scene/MetadataPickingSpec.js @@ -0,0 +1,1492 @@ +import { + Cartesian2, + Cartesian3, + Cartesian4, + Matrix2, + Matrix3, + Matrix4, + MetadataComponentType, + MetadataPicking, + MetadataType, +} from "../../index.js"; + +// The precision for Jasmine `toBeCloseTo` calls. Note that this +// is not an "epsilon", but described as "The number of decimal +// points to check" ... +const precision = 3; + +describe("Scene/MetadataPicking", function () { + describe("decodeRawMetadataValue", function () { + it("throws for invalid component type", function () { + expect(function () { + const componentType = "NOT_A_COMPONENT_TYPE"; + const array = new Uint8Array([12, 23]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const index = 0; + MetadataPicking.decodeRawMetadataValue(componentType, dataView, index); + }).toThrowError(); + }); + + it("throws for invalid index", function () { + expect(function () { + const componentType = MetadataComponentType.INT8; + const array = new Uint8Array([12, 23]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const index = 1234; + MetadataPicking.decodeRawMetadataValue(componentType, dataView, index); + }).toThrowError(); + }); + + it("decodes raw value with component type INT8", function () { + const array = new Int8Array([12, 23]); + const componentType = MetadataComponentType.INT8; + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const index = 1; + const value = MetadataPicking.decodeRawMetadataValue( + componentType, + dataView, + index, + ); + expect(value).toBe(23); + }); + + it("decodes raw value with component type UINT8", function () { + const array = new Uint8Array([12, 23]); + const componentType = MetadataComponentType.UINT8; + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const index = 1; + const value = MetadataPicking.decodeRawMetadataValue( + componentType, + dataView, + index, + ); + expect(value).toBe(23); + }); + + it("decodes raw value with component type INT16", function () { + const array = new Int16Array([1234, 2345]); + const componentType = MetadataComponentType.INT16; + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const index = 2; + const value = MetadataPicking.decodeRawMetadataValue( + componentType, + dataView, + index, + ); + expect(value).toBe(2345); + }); + + it("decodes raw value with component type UINT16", function () { + const array = new Uint16Array([1234, 2345]); + const componentType = MetadataComponentType.UINT16; + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const index = 2; + const value = MetadataPicking.decodeRawMetadataValue( + componentType, + dataView, + index, + ); + expect(value).toBe(2345); + }); + + it("decodes raw value with component type INT32", function () { + const array = new Int32Array([123456, 234567]); + const componentType = MetadataComponentType.INT32; + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const index = 4; + const value = MetadataPicking.decodeRawMetadataValue( + componentType, + dataView, + index, + ); + expect(value).toBe(234567); + }); + + it("decodes raw value with component type UINT32", function () { + const array = new Uint32Array([123456, 234567]); + const componentType = MetadataComponentType.UINT32; + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const index = 4; + const value = MetadataPicking.decodeRawMetadataValue( + componentType, + dataView, + index, + ); + expect(value).toBe(234567); + }); + + it("decodes raw value with component type INT64", function () { + const array = new BigInt64Array([12345678900n, 23456789000n]); + const componentType = MetadataComponentType.INT64; + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const index = 8; + const value = MetadataPicking.decodeRawMetadataValue( + componentType, + dataView, + index, + ); + expect(value).toBe(23456789000n); + }); + + it("decodes raw value with component type UINT64", function () { + const array = new BigUint64Array([12345678900n, 23456789000n]); + const componentType = MetadataComponentType.UINT64; + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const index = 8; + const value = MetadataPicking.decodeRawMetadataValue( + componentType, + dataView, + index, + ); + expect(value).toBe(23456789000n); + }); + + it("decodes raw value with component type FLOAT32", function () { + const array = new Float32Array([1.23, 2.34]); + const componentType = MetadataComponentType.FLOAT32; + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const index = 4; + const value = MetadataPicking.decodeRawMetadataValue( + componentType, + dataView, + index, + ); + expect(value).toBeCloseTo(2.34, precision); + }); + + it("decodes raw value with component type FLOAT64", function () { + const array = new Float64Array([1.23, 2.34]); + const componentType = MetadataComponentType.FLOAT64; + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const index = 8; + const value = MetadataPicking.decodeRawMetadataValue( + componentType, + dataView, + index, + ); + expect(value).toBeCloseTo(2.34, precision); + }); + }); + + describe("decodeRawMetadataValueComponent", function () { + it("throws for invalid component type", function () { + expect(function () { + const classProperty = { + componentType: "NOT_A_COMPONENT_TYPE", + normalized: false, + }; + const array = new Uint8Array([12, 23]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const dataViewOffset = 0; + MetadataPicking.decodeRawMetadataValueComponent( + classProperty, + dataView, + dataViewOffset, + ); + }).toThrowError(); + }); + + it("throws for invalid offset", function () { + expect(function () { + const classProperty = { + componentType: MetadataComponentType.INT8, + normalized: false, + }; + const array = new Uint8Array([12, 23]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const dataViewOffset = 1234; + MetadataPicking.decodeRawMetadataValueComponent( + classProperty, + dataView, + dataViewOffset, + ); + }).toThrowError(); + }); + + it("decodes component with componentType INT8", function () { + const classProperty = { + componentType: MetadataComponentType.INT8, + normalized: false, + }; + const array = new Uint8Array([12, 23]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const dataViewOffset = 1; + const value = MetadataPicking.decodeRawMetadataValueComponent( + classProperty, + dataView, + dataViewOffset, + ); + expect(value).toBe(23); + }); + + it("decodes component with componentType normalized INT8", function () { + const classProperty = { + componentType: MetadataComponentType.INT8, + normalized: true, + }; + const array = new Uint8Array([12, 23]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const dataViewOffset = 1; + const value = MetadataPicking.decodeRawMetadataValueComponent( + classProperty, + dataView, + dataViewOffset, + ); + expect(value).toBeCloseTo(23 / 127.0, precision); + }); + + it("decodes component with componentType UINT8", function () { + const classProperty = { + componentType: MetadataComponentType.UINT8, + normalized: false, + }; + const array = new Uint8Array([12, 23]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const dataViewOffset = 1; + const value = MetadataPicking.decodeRawMetadataValueComponent( + classProperty, + dataView, + dataViewOffset, + ); + expect(value).toBe(23); + }); + + it("decodes component with componentType normalized UINT8", function () { + const classProperty = { + componentType: MetadataComponentType.UINT8, + normalized: true, + }; + const array = new Uint8Array([12, 23]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const dataViewOffset = 1; + const value = MetadataPicking.decodeRawMetadataValueComponent( + classProperty, + dataView, + dataViewOffset, + ); + expect(value).toBeCloseTo(23 / 255.0, precision); + }); + + it("decodes component with componentType INT16", function () { + const classProperty = { + componentType: MetadataComponentType.INT16, + normalized: false, + }; + const array = new Int16Array([1234, 2345]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const dataViewOffset = 2; + const value = MetadataPicking.decodeRawMetadataValueComponent( + classProperty, + dataView, + dataViewOffset, + ); + expect(value).toBe(2345); + }); + + it("decodes component with componentType normalized INT16", function () { + const classProperty = { + componentType: MetadataComponentType.INT16, + normalized: true, + }; + const array = new Int16Array([1234, 2345]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const dataViewOffset = 2; + const value = MetadataPicking.decodeRawMetadataValueComponent( + classProperty, + dataView, + dataViewOffset, + ); + expect(value).toBeCloseTo(2345 / 32767.0, precision); + }); + + it("decodes component with componentType UINT16", function () { + const classProperty = { + componentType: MetadataComponentType.UINT16, + normalized: false, + }; + const array = new Uint16Array([1234, 2345]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const dataViewOffset = 2; + const value = MetadataPicking.decodeRawMetadataValueComponent( + classProperty, + dataView, + dataViewOffset, + ); + expect(value).toBe(2345); + }); + + it("decodes component with componentType normalized UINT16", function () { + const classProperty = { + componentType: MetadataComponentType.UINT16, + normalized: true, + }; + const array = new Uint16Array([1234, 2345]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const dataViewOffset = 2; + const value = MetadataPicking.decodeRawMetadataValueComponent( + classProperty, + dataView, + dataViewOffset, + ); + expect(value).toBeCloseTo(2345 / 65535.0, precision); + }); + + it("decodes component with componentType INT32", function () { + const classProperty = { + componentType: MetadataComponentType.INT32, + normalized: false, + }; + const array = new Int32Array([123456, 234567]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const dataViewOffset = 4; + const value = MetadataPicking.decodeRawMetadataValueComponent( + classProperty, + dataView, + dataViewOffset, + ); + expect(value).toBe(234567); + }); + + it("decodes component with componentType normalized INT32", function () { + const classProperty = { + componentType: MetadataComponentType.INT32, + normalized: true, + }; + const array = new Int32Array([123456, 234567]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const dataViewOffset = 4; + const value = MetadataPicking.decodeRawMetadataValueComponent( + classProperty, + dataView, + dataViewOffset, + ); + expect(value).toBeCloseTo(234567 / 2147483647.0, precision); + }); + + it("decodes component with componentType UINT32", function () { + const classProperty = { + componentType: MetadataComponentType.UINT32, + normalized: false, + }; + const array = new Uint32Array([123456, 234567]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const dataViewOffset = 4; + const value = MetadataPicking.decodeRawMetadataValueComponent( + classProperty, + dataView, + dataViewOffset, + ); + expect(value).toBe(234567); + }); + + it("decodes component with componentType normalized UINT32", function () { + const classProperty = { + componentType: MetadataComponentType.UINT32, + normalized: true, + }; + const array = new Uint32Array([123456, 234567]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const dataViewOffset = 4; + const value = MetadataPicking.decodeRawMetadataValueComponent( + classProperty, + dataView, + dataViewOffset, + ); + expect(value).toBeCloseTo(234567 / 4294967295.0, precision); + }); + + it("decodes component with componentType INT64", function () { + const classProperty = { + componentType: MetadataComponentType.INT64, + normalized: false, + }; + const array = new BigInt64Array([12345678900n, 23456789000n]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const dataViewOffset = 8; + const value = MetadataPicking.decodeRawMetadataValueComponent( + classProperty, + dataView, + dataViewOffset, + ); + expect(value).toBe(23456789000n); + }); + + it("decodes component with componentType normalized INT64", function () { + const classProperty = { + componentType: MetadataComponentType.INT64, + normalized: true, + }; + const array = new BigInt64Array([12345678900n, 23456789000n]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const dataViewOffset = 8; + const value = MetadataPicking.decodeRawMetadataValueComponent( + classProperty, + dataView, + dataViewOffset, + ); + expect(Number(value)).toBeCloseTo( + Number(23456789000n / 9223372036854775807n), + precision, + ); + }); + + it("decodes component with componentType UINT64", function () { + const classProperty = { + componentType: MetadataComponentType.UINT64, + normalized: false, + }; + const array = new BigUint64Array([12345678900n, 23456789000n]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const dataViewOffset = 8; + const value = MetadataPicking.decodeRawMetadataValueComponent( + classProperty, + dataView, + dataViewOffset, + ); + expect(value).toBe(23456789000n); + }); + + it("decodes component with componentType normalized UINT64", function () { + const classProperty = { + componentType: MetadataComponentType.UINT64, + normalized: true, + }; + const array = new BigUint64Array([12345678900n, 23456789000n]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const dataViewOffset = 8; + const value = MetadataPicking.decodeRawMetadataValueComponent( + classProperty, + dataView, + dataViewOffset, + ); + expect(Number(value)).toBeCloseTo( + Number(23456789000n / 18446744073709551615n), + precision, + ); + }); + + it("decodes component with componentType FLOAT32", function () { + const classProperty = { + componentType: MetadataComponentType.FLOAT32, + normalized: false, + }; + const array = new Float32Array([1.23, 2.34]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const dataViewOffset = 4; + const value = MetadataPicking.decodeRawMetadataValueComponent( + classProperty, + dataView, + dataViewOffset, + ); + expect(value).toBeCloseTo(2.34, precision); + }); + + it("decodes component with componentType FLOAT64", function () { + const classProperty = { + componentType: MetadataComponentType.FLOAT64, + normalized: false, + }; + const array = new Float64Array([1.23, 2.34]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const dataViewOffset = 8; + const value = MetadataPicking.decodeRawMetadataValueComponent( + classProperty, + dataView, + dataViewOffset, + ); + expect(value).toBeCloseTo(2.34, precision); + }); + }); + + describe("decodeRawMetadataValueElement", function () { + it("throws for invalid component type", function () { + expect(function () { + const classProperty = { + type: "SCALAR", + componentType: "NOT_A_COMPONENT_TYPE", + normalized: false, + }; + const array = new Int8Array([12, 23]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const elementIndex = 0; + MetadataPicking.decodeRawMetadataValueElement( + classProperty, + dataView, + elementIndex, + ); + }).toThrowError(); + }); + + it("throws for invalid element index", function () { + expect(function () { + const classProperty = { + type: "SCALAR", + componentType: MetadataComponentType.INT8, + normalized: false, + }; + const array = new Int8Array([12, 23]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const elementIndex = 1234; + MetadataPicking.decodeRawMetadataValueElement( + classProperty, + dataView, + elementIndex, + ); + }).toThrowError(); + }); + + it("decodes element with type SCALAR and component type INT8", function () { + const classProperty = { + type: MetadataType.SCALAR, + componentType: MetadataComponentType.INT8, + normalized: false, + }; + const array = new Int8Array([0, 1]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const elementIndex = 1; + const value = MetadataPicking.decodeRawMetadataValueElement( + classProperty, + dataView, + elementIndex, + ); + expect(value).toBe(1); + }); + + it("decodes element with type VEC2 and component type INT8", function () { + const classProperty = { + type: MetadataType.VEC2, + componentType: MetadataComponentType.INT8, + normalized: false, + }; + const array = new Int8Array([0, 1, 2, 3]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const elementIndex = 1; + const value = MetadataPicking.decodeRawMetadataValueElement( + classProperty, + dataView, + elementIndex, + ); + expect(value).toEqual([2, 3]); + }); + + it("decodes element with type VEC3 and component type INT8", function () { + const classProperty = { + type: MetadataType.VEC3, + componentType: MetadataComponentType.INT8, + normalized: false, + }; + const array = new Int8Array([0, 1, 2, 3, 4, 5]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const elementIndex = 1; + const value = MetadataPicking.decodeRawMetadataValueElement( + classProperty, + dataView, + elementIndex, + ); + expect(value).toEqual([3, 4, 5]); + }); + + it("decodes element with type VEC4 and component type INT8", function () { + const classProperty = { + type: MetadataType.VEC4, + componentType: MetadataComponentType.INT8, + normalized: false, + }; + const array = new Int8Array([0, 1, 2, 3, 4, 5, 6, 7]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const elementIndex = 1; + const value = MetadataPicking.decodeRawMetadataValueElement( + classProperty, + dataView, + elementIndex, + ); + expect(value).toEqual([4, 5, 6, 7]); + }); + + it("decodes element with type MAT2 and component type INT8", function () { + const classProperty = { + type: MetadataType.MAT2, + componentType: MetadataComponentType.INT8, + normalized: false, + }; + const array = new Int8Array([0, 1, 2, 3, 4, 5, 6, 7]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const elementIndex = 1; + const value = MetadataPicking.decodeRawMetadataValueElement( + classProperty, + dataView, + elementIndex, + ); + expect(value).toEqual([4, 5, 6, 7]); + }); + + it("decodes element with type MAT3 and component type INT8", function () { + const classProperty = { + type: MetadataType.MAT3, + componentType: MetadataComponentType.INT8, + normalized: false, + }; + const array = new Int8Array([ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + ]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const elementIndex = 1; + const value = MetadataPicking.decodeRawMetadataValueElement( + classProperty, + dataView, + elementIndex, + ); + expect(value).toEqual([9, 10, 11, 12, 13, 14, 15, 16, 17]); + }); + + it("decodes element with type MAT4 and component type INT8", function () { + const classProperty = { + type: MetadataType.MAT4, + componentType: MetadataComponentType.INT8, + normalized: false, + }; + const array = new Int8Array([ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + ]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const elementIndex = 1; + const value = MetadataPicking.decodeRawMetadataValueElement( + classProperty, + dataView, + elementIndex, + ); + expect(value).toEqual([ + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + ]); + }); + + it("decodes element with type SCALAR and component type FLOAT32", function () { + const classProperty = { + type: MetadataType.SCALAR, + componentType: MetadataComponentType.FLOAT32, + normalized: false, + }; + const array = new Float32Array([0, 1]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const elementIndex = 1; + const value = MetadataPicking.decodeRawMetadataValueElement( + classProperty, + dataView, + elementIndex, + ); + expect(value).toBe(1); + }); + + it("decodes element with type VEC2 and component type FLOAT32", function () { + const classProperty = { + type: MetadataType.VEC2, + componentType: MetadataComponentType.FLOAT32, + normalized: false, + }; + const array = new Float32Array([0, 1, 2, 3]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const elementIndex = 1; + const value = MetadataPicking.decodeRawMetadataValueElement( + classProperty, + dataView, + elementIndex, + ); + expect(value).toEqual([2, 3]); + }); + + it("decodes element with type VEC3 and component type FLOAT32", function () { + const classProperty = { + type: MetadataType.VEC3, + componentType: MetadataComponentType.FLOAT32, + normalized: false, + }; + const array = new Float32Array([0, 1, 2, 3, 4, 5]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const elementIndex = 1; + const value = MetadataPicking.decodeRawMetadataValueElement( + classProperty, + dataView, + elementIndex, + ); + expect(value).toEqual([3, 4, 5]); + }); + + it("decodes element with type VEC4 and component type FLOAT32", function () { + const classProperty = { + type: MetadataType.VEC4, + componentType: MetadataComponentType.FLOAT32, + normalized: false, + }; + const array = new Float32Array([0, 1, 2, 3, 4, 5, 6, 7]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const elementIndex = 1; + const value = MetadataPicking.decodeRawMetadataValueElement( + classProperty, + dataView, + elementIndex, + ); + expect(value).toEqual([4, 5, 6, 7]); + }); + + it("decodes element with type MAT2 and component type FLOAT32", function () { + const classProperty = { + type: MetadataType.MAT2, + componentType: MetadataComponentType.FLOAT32, + normalized: false, + }; + const array = new Float32Array([0, 1, 2, 3, 4, 5, 6, 7]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const elementIndex = 1; + const value = MetadataPicking.decodeRawMetadataValueElement( + classProperty, + dataView, + elementIndex, + ); + expect(value).toEqual([4, 5, 6, 7]); + }); + + it("decodes element with type MAT3 and component type FLOAT32", function () { + const classProperty = { + type: MetadataType.MAT3, + componentType: MetadataComponentType.FLOAT32, + normalized: false, + }; + const array = new Float32Array([ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + ]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const elementIndex = 1; + const value = MetadataPicking.decodeRawMetadataValueElement( + classProperty, + dataView, + elementIndex, + ); + expect(value).toEqual([9, 10, 11, 12, 13, 14, 15, 16, 17]); + }); + + it("decodes element with type MAT4 and component type FLOAT32", function () { + const classProperty = { + type: MetadataType.MAT4, + componentType: MetadataComponentType.FLOAT32, + normalized: false, + }; + const array = new Float32Array([ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + ]); + const dataView = new DataView( + array.buffer, + array.byteOffset, + array.byteLength, + ); + const elementIndex = 1; + const value = MetadataPicking.decodeRawMetadataValueElement( + classProperty, + dataView, + elementIndex, + ); + expect(value).toEqual([ + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + ]); + }); + }); + + describe("decodeRawMetadataValues", function () { + it("throws for invalid component type", function () { + expect(function () { + const classProperty = { + type: "SCALAR", + componentType: "NOT_A_COMPONENT_TYPE", + normalized: false, + }; + const rawPixelValues = new Uint8Array([0, 1, 2, 3]); + MetadataPicking.decodeRawMetadataValues(classProperty, rawPixelValues); + }).toThrowError(); + }); + + it("throws for invalid input length", function () { + expect(function () { + const classProperty = { + type: MetadataType.VEC2, + componentType: MetadataComponentType.UINT8, + normalized: false, + }; + const rawPixelValues = new Uint8Array([0]); + MetadataPicking.decodeRawMetadataValues(classProperty, rawPixelValues); + }).toThrowError(); + }); + + it("decodes raw values with type SCALAR and component type INT8", function () { + const classProperty = { + type: MetadataType.SCALAR, + componentType: MetadataComponentType.INT8, + normalized: false, + }; + const rawPixelValues = new Uint8Array([12, 23, 34, 45]); + const value = MetadataPicking.decodeRawMetadataValues( + classProperty, + rawPixelValues, + ); + expect(value).toEqual(12); + }); + + it("decodes raw values with type VEC2 and component type INT8", function () { + const classProperty = { + type: MetadataType.VEC2, + componentType: MetadataComponentType.INT8, + normalized: false, + }; + const rawPixelValues = new Uint8Array([0, 1, 2, 3]); + const value = MetadataPicking.decodeRawMetadataValues( + classProperty, + rawPixelValues, + ); + expect(value).toEqual([0, 1]); + }); + + it("decodes raw values with type VEC3 and component type INT8", function () { + const classProperty = { + type: MetadataType.VEC3, + componentType: MetadataComponentType.INT8, + normalized: false, + }; + const rawPixelValues = new Uint8Array([0, 1, 2, 3]); + const value = MetadataPicking.decodeRawMetadataValues( + classProperty, + rawPixelValues, + ); + expect(value).toEqual([0, 1, 2]); + }); + + it("decodes raw values with type VEC4 and component type INT8", function () { + const classProperty = { + type: MetadataType.VEC4, + componentType: MetadataComponentType.INT8, + normalized: false, + }; + const rawPixelValues = new Uint8Array([0, 1, 2, 3]); + const value = MetadataPicking.decodeRawMetadataValues( + classProperty, + rawPixelValues, + ); + expect(value).toEqual([0, 1, 2, 3]); + }); + }); + + describe("convertToObjectType", function () { + it("throws for invalid type", function () { + expect(function () { + const type = "NOT_A_TYPE"; + const value = 0; + MetadataPicking.convertToObjectType(type, value); + }).toThrowError(); + }); + + it("returns SCALAR, STRING, BOOLEAN, and ENUM types (and arrays thereof) unmodified", function () { + const value0 = MetadataPicking.convertToObjectType( + MetadataType.SCALAR, + 123, + ); + const value1 = MetadataPicking.convertToObjectType( + MetadataType.STRING, + "Value", + ); + const value2 = MetadataPicking.convertToObjectType( + MetadataType.SCALAR, + true, + ); + const value3 = MetadataPicking.convertToObjectType( + MetadataType.ENUM, + "VALUE", + ); + + expect(value0).toEqual(123); + expect(value1).toEqual("Value"); + expect(value2).toEqual(true); + expect(value3).toEqual("VALUE"); + + const value0a = MetadataPicking.convertToObjectType( + MetadataType.SCALAR, + [123, 234], + ); + const value1a = MetadataPicking.convertToObjectType(MetadataType.STRING, [ + "Value0", + "Value1", + ]); + const value2a = MetadataPicking.convertToObjectType(MetadataType.SCALAR, [ + true, + false, + ]); + const value3a = MetadataPicking.convertToObjectType(MetadataType.ENUM, [ + "VALUE_A", + "VALUE_B", + ]); + + expect(value0a).toEqual([123, 234]); + expect(value1a).toEqual(["Value0", "Value1"]); + expect(value2a).toEqual([true, false]); + expect(value3a).toEqual(["VALUE_A", "VALUE_B"]); + }); + + it("converts array-based VEC2 input into a Cartesian2", function () { + const array = [0, 1]; + const expected = Cartesian2.unpack(array, 0, new Cartesian2()); + const actual = MetadataPicking.convertToObjectType( + MetadataType.VEC2, + array, + ); + expect(actual).toEqual(expected); + }); + + it("converts array-based VEC3 input into a Cartesian3", function () { + const array = [0, 1, 2]; + const expected = Cartesian3.unpack(array, 0, new Cartesian3()); + const actual = MetadataPicking.convertToObjectType( + MetadataType.VEC3, + array, + ); + expect(actual).toEqual(expected); + }); + + it("converts array-based VEC4 input into a Cartesian4", function () { + const array = [0, 1, 2, 3]; + const expected = Cartesian4.unpack(array, 0, new Cartesian4()); + const actual = MetadataPicking.convertToObjectType( + MetadataType.VEC4, + array, + ); + expect(actual).toEqual(expected); + }); + + it("converts array-based MAT2 input into a Matrix2", function () { + const array = [0, 1, 2, 3]; + const expected = Matrix2.unpack(array, 0, new Matrix2()); + const actual = MetadataPicking.convertToObjectType( + MetadataType.MAT2, + array, + ); + expect(actual).toEqual(expected); + }); + + it("converts array-based MAT3 input into a Matrix3", function () { + const array = [0, 1, 2, 3, 4, 5, 6, 7, 8]; + const expected = Matrix3.unpack(array, 0, new Matrix3()); + const actual = MetadataPicking.convertToObjectType( + MetadataType.MAT3, + array, + ); + expect(actual).toEqual(expected); + }); + + it("converts array-based MAT4 input into a Matrix4", function () { + const array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; + const expected = Matrix4.unpack(array, 0, new Matrix4()); + const actual = MetadataPicking.convertToObjectType( + MetadataType.MAT4, + array, + ); + expect(actual).toEqual(expected); + }); + }); + + describe("convertFromObjectType", function () { + it("throws for invalid type", function () { + expect(function () { + const type = "NOT_A_TYPE"; + const value = new Cartesian2(); + MetadataPicking.convertFromObjectType(type, value); + }).toThrowError(); + }); + + it("returns SCALAR, STRING, BOOLEAN, and ENUM types (and arrays thereof) unmodified", function () { + const value0 = MetadataPicking.convertFromObjectType( + MetadataType.SCALAR, + 123, + ); + const value1 = MetadataPicking.convertFromObjectType( + MetadataType.STRING, + "Value", + ); + const value2 = MetadataPicking.convertFromObjectType( + MetadataType.SCALAR, + true, + ); + const value3 = MetadataPicking.convertFromObjectType( + MetadataType.ENUM, + "VALUE", + ); + + expect(value0).toEqual(123); + expect(value1).toEqual("Value"); + expect(value2).toEqual(true); + expect(value3).toEqual("VALUE"); + + const value0a = MetadataPicking.convertFromObjectType( + MetadataType.SCALAR, + [123, 234], + ); + const value1a = MetadataPicking.convertFromObjectType( + MetadataType.STRING, + ["Value0", "Value1"], + ); + const value2a = MetadataPicking.convertFromObjectType( + MetadataType.SCALAR, + [true, false], + ); + const value3a = MetadataPicking.convertFromObjectType(MetadataType.ENUM, [ + "VALUE_A", + "VALUE_B", + ]); + + expect(value0a).toEqual([123, 234]); + expect(value1a).toEqual(["Value0", "Value1"]); + expect(value2a).toEqual([true, false]); + expect(value3a).toEqual(["VALUE_A", "VALUE_B"]); + }); + + it("converts Cartesian2 into array-based VEC2", function () { + const expected = [0, 1]; + const value = Cartesian2.unpack(expected, 0, new Cartesian2()); + const actual = MetadataPicking.convertFromObjectType( + MetadataType.VEC2, + value, + ); + expect(actual).toEqual(expected); + }); + + it("converts Cartesian3 into array-based VEC3", function () { + const expected = [0, 1, 2]; + const value = Cartesian3.unpack(expected, 0, new Cartesian3()); + const actual = MetadataPicking.convertFromObjectType( + MetadataType.VEC3, + value, + ); + expect(actual).toEqual(expected); + }); + + it("converts Cartesian4 into array-based VEC4", function () { + const expected = [0, 1, 2, 3]; + const value = Cartesian4.unpack(expected, 0, new Cartesian4()); + const actual = MetadataPicking.convertFromObjectType( + MetadataType.VEC4, + value, + ); + expect(actual).toEqual(expected); + }); + + it("converts Matrix2 into array-based MAT2", function () { + const expected = [0, 1, 2, 3]; + const value = Matrix2.unpack(expected, 0, new Matrix2()); + const actual = MetadataPicking.convertFromObjectType( + MetadataType.MAT2, + value, + ); + expect(actual).toEqual(expected); + }); + + it("converts Matrix3 into array-based MAT3", function () { + const expected = [0, 1, 2, 3, 4, 5, 6, 7, 8]; + const value = Matrix3.unpack(expected, 0, new Matrix3()); + const actual = MetadataPicking.convertFromObjectType( + MetadataType.MAT3, + value, + ); + expect(actual).toEqual(expected); + }); + + it("converts Matrix4 into array-based MAT4", function () { + const expected = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; + const value = Matrix4.unpack(expected, 0, new Matrix4()); + const actual = MetadataPicking.convertFromObjectType( + MetadataType.MAT4, + value, + ); + expect(actual).toEqual(expected); + }); + }); + + describe("decodeMetadataValues", function () { + it("throws for invalid type", function () { + expect(function () { + const classProperty = { + type: "NOT_A_TYPE", + componentType: MetadataComponentType.UINT8, + normalized: false, + }; + const metadataProperty = { + hasValueTransform: false, + }; + const rawPixelValues = new Uint8Array([0, 1, 2, 3]); + MetadataPicking.decodeMetadataValues( + classProperty, + metadataProperty, + rawPixelValues, + ); + }).toThrowError(); + }); + + it("decodes values with type SCALAR and component type INT8", function () { + const classProperty = { + type: MetadataType.SCALAR, + componentType: MetadataComponentType.INT8, + normalized: false, + }; + const metadataProperty = { + hasValueTransform: false, + }; + const rawPixelValues = new Uint8Array([12, 23, 34, 45]); + const value = MetadataPicking.decodeMetadataValues( + classProperty, + metadataProperty, + rawPixelValues, + ); + expect(value).toEqual(12); + }); + + it("decodes values with type VEC2 and component type INT8", function () { + const classProperty = { + type: MetadataType.VEC2, + componentType: MetadataComponentType.INT8, + normalized: false, + }; + const metadataProperty = { + hasValueTransform: false, + }; + const rawPixelValues = new Uint8Array([12, 23, 34, 45]); + const value = MetadataPicking.decodeMetadataValues( + classProperty, + metadataProperty, + rawPixelValues, + ); + expect(value).toEqual(new Cartesian2(12, 23)); + }); + + it("decodes values with type VEC3 and component type INT8", function () { + const classProperty = { + type: MetadataType.VEC3, + componentType: MetadataComponentType.INT8, + normalized: false, + }; + const metadataProperty = { + hasValueTransform: false, + }; + const rawPixelValues = new Uint8Array([12, 23, 34, 45]); + const value = MetadataPicking.decodeMetadataValues( + classProperty, + metadataProperty, + rawPixelValues, + ); + expect(value).toEqual(new Cartesian3(12, 23, 34)); + }); + + it("decodes values with type VEC4 and component type INT8", function () { + const classProperty = { + type: MetadataType.VEC4, + componentType: MetadataComponentType.INT8, + normalized: false, + }; + const metadataProperty = { + hasValueTransform: false, + }; + const rawPixelValues = new Uint8Array([12, 23, 34, 45]); + const value = MetadataPicking.decodeMetadataValues( + classProperty, + metadataProperty, + rawPixelValues, + ); + expect(value).toEqual(new Cartesian4(12, 23, 34, 45)); + }); + + it("decodes values with type SCALAR and component type INT8 and offset and scale", function () { + const classProperty = { + type: MetadataType.SCALAR, + componentType: MetadataComponentType.INT8, + normalized: false, + }; + const offset = 100; + const scale = 2; + const metadataProperty = { + hasValueTransform: true, + offset: offset, + scale: scale, + }; + const rawPixelValues = new Uint8Array([12, 23, 34, 45]); + const value = MetadataPicking.decodeMetadataValues( + classProperty, + metadataProperty, + rawPixelValues, + ); + expect(value).toEqual(offset + 12 * scale); + }); + + it("decodes values with type VEC2 and component type INT8 and offset and scale", function () { + const classProperty = { + type: MetadataType.VEC2, + componentType: MetadataComponentType.INT8, + normalized: false, + }; + const offset = new Cartesian2(100, 200); + const scale = new Cartesian2(2, 3); + const metadataProperty = { + hasValueTransform: true, + offset: offset, + scale: scale, + }; + const rawPixelValues = new Uint8Array([12, 23, 34, 45]); + const value = MetadataPicking.decodeMetadataValues( + classProperty, + metadataProperty, + rawPixelValues, + ); + const expected = new Cartesian2( + offset.x + 12 * scale.x, + offset.y + 23 * scale.y, + ); + expect(value).toEqual(expected); + }); + + it("decodes values with type VEC3 and component type INT8 and offset and scale", function () { + const classProperty = { + type: MetadataType.VEC3, + componentType: MetadataComponentType.INT8, + normalized: false, + }; + const offset = new Cartesian3(100, 200, 300); + const scale = new Cartesian3(2, 3, 4); + const metadataProperty = { + hasValueTransform: true, + offset: offset, + scale: scale, + }; + const rawPixelValues = new Uint8Array([12, 23, 34, 45]); + const value = MetadataPicking.decodeMetadataValues( + classProperty, + metadataProperty, + rawPixelValues, + ); + const expected = new Cartesian3( + offset.x + 12 * scale.x, + offset.y + 23 * scale.y, + offset.z + 34 * scale.z, + ); + expect(value).toEqual(expected); + }); + + it("decodes values with type VEC4 and component type INT8 and offset and scale", function () { + const classProperty = { + type: MetadataType.VEC4, + componentType: MetadataComponentType.INT8, + normalized: false, + }; + const offset = new Cartesian4(100, 200, 300, 400); + const scale = new Cartesian4(2, 3, 4, 5); + const metadataProperty = { + hasValueTransform: true, + offset: offset, + scale: scale, + }; + const rawPixelValues = new Uint8Array([12, 23, 34, 45]); + const value = MetadataPicking.decodeMetadataValues( + classProperty, + metadataProperty, + rawPixelValues, + ); + const expected = new Cartesian4( + offset.x + 12 * scale.x, + offset.y + 23 * scale.y, + offset.z + 34 * scale.z, + offset.w + 45 * scale.w, + ); + expect(value).toEqual(expected); + }); + }); +}); diff --git a/packages/engine/Specs/Scene/SceneSpec.js b/packages/engine/Specs/Scene/SceneSpec.js index a2f21c78969..3e5c7dda20b 100644 --- a/packages/engine/Specs/Scene/SceneSpec.js +++ b/packages/engine/Specs/Scene/SceneSpec.js @@ -271,26 +271,64 @@ function createPropertyTextureGltfScalar() { type: "SCALAR", componentType: "UINT8", }, + }, + }, + }, + }; + const properties = { + example_UINT8_SCALAR: { + index: 0, + texCoord: 0, + channels: [0], + }, + }; + return createPropertyTextureGltf(schema, properties); +} + +/** + * Creates the glTF for the normalized 'scalar' test case + * + * @param {number|undefined} classPropertyOffset The optional offset + * that will be defined in the class property definition + * @param {number|undefined} classPropertyScale The optional scale + * that will be defined in the class property definition + * @param {number|undefined} metadataPropertyOffset The optional offset + * that will be defined in the property texture property definition + * @param {number|undefined} metadataPropertyScale The optional scale + * that will be defined in the property texture property definition + * @returns The glTF + */ +function createPropertyTextureGltfNormalizedScalar( + classPropertyOffset, + classPropertyScale, + metadataPropertyOffset, + metadataPropertyScale, +) { + const schema = { + id: "ExampleSchema", + classes: { + exampleClass: { + name: "Example class", + properties: { example_normalized_UINT8_SCALAR: { name: "Example SCALAR property with normalized UINT8 components", type: "SCALAR", componentType: "UINT8", normalized: true, + offset: classPropertyOffset, + scale: classPropertyScale, }, }, }, }, }; const properties = { - example_UINT8_SCALAR: { - index: 0, - texCoord: 0, - channels: [0], - }, example_normalized_UINT8_SCALAR: { index: 0, texCoord: 0, - channels: [1], + channels: [0], + offset: metadataPropertyOffset, + scale: metadataPropertyScale, }, }; return createPropertyTextureGltf(schema, properties); @@ -329,6 +367,57 @@ function createPropertyTextureGltfScalarArray() { return createPropertyTextureGltf(schema, properties); } +/** + * Creates the glTF for the 'normalized scalar array' test case + * + * @param {number[]|undefined} classPropertyOffset The optional offset + * that will be defined in the class property definition + * @param {number[]|undefined} classPropertyScale The optional scale + * that will be defined in the class property definition + * @param {number[]|undefined} metadataPropertyOffset The optional offset + * that will be defined in the property texture property definition + * @param {number[]|undefined} metadataPropertyScale The optional scale + * that will be defined in the property texture property definition + * @returns The glTF + */ +function createPropertyTextureGltfNormalizedScalarArray( + classPropertyOffset, + classPropertyScale, + metadataPropertyOffset, + metadataPropertyScale, +) { + const schema = { + id: "ExampleSchema", + classes: { + exampleClass: { + name: "Example class", + properties: { + example_fixed_length_normalized_UINT8_SCALAR_array: { + name: "Example fixed-length SCALAR array property with normalized INT8 components", + type: "SCALAR", + componentType: "UINT8", + array: true, + count: 3, + normalized: true, + offset: classPropertyOffset, + scale: classPropertyScale, + }, + }, + }, + }, + }; + const properties = { + example_fixed_length_normalized_UINT8_SCALAR_array: { + index: 0, + texCoord: 0, + channels: [0, 1, 2], + offset: metadataPropertyOffset, + scale: metadataPropertyScale, + }, + }; + return createPropertyTextureGltf(schema, properties); +} + /** * Creates the glTF for the 'vec2' test case * @@ -363,9 +452,22 @@ function createPropertyTextureGltfVec2() { /** * Creates the glTF for the normalized 'vec2' test case * + * @param {number[]|undefined} classPropertyOffset The optional offset + * that will be defined in the class property definition + * @param {number[]|undefined} classPropertyScale The optional scale + * that will be defined in the class property definition + * @param {number[]|undefined} metadataPropertyOffset The optional offset + * that will be defined in the property texture property definition + * @param {number[]|undefined} metadataPropertyScale The optional scale + * that will be defined in the property texture property definition * @returns The glTF */ -function createPropertyTextureGltfNormalizedVec2() { +function createPropertyTextureGltfNormalizedVec2( + classPropertyOffset, + classPropertyScale, + metadataPropertyOffset, + metadataPropertyScale, +) { const schema = { id: "ExampleSchema", classes: { @@ -377,6 +479,8 @@ function createPropertyTextureGltfNormalizedVec2() { type: "VEC2", componentType: "UINT8", normalized: true, + offset: classPropertyOffset, + scale: classPropertyScale, }, }, }, @@ -387,6 +491,8 @@ function createPropertyTextureGltfNormalizedVec2() { index: 0, texCoord: 0, channels: [0, 1], + offset: metadataPropertyOffset, + scale: metadataPropertyScale, }, }; return createPropertyTextureGltf(schema, properties); @@ -3114,7 +3220,16 @@ describe( const schemaId = undefined; const className = "exampleClass"; const propertyName = "example_normalized_UINT8_SCALAR"; - const gltf = createPropertyTextureGltfScalar(); + const classPropertyOffset = undefined; + const classPropertyScale = undefined; + const metadataPropertyOffset = undefined; + const metadataPropertyScale = undefined; + const gltf = createPropertyTextureGltfNormalizedScalar( + classPropertyOffset, + classPropertyScale, + metadataPropertyOffset, + metadataPropertyScale, + ); const canvasSizeX = textureSizeX * canvasScaling; const canvasSizeY = textureSizeY * canvasScaling; @@ -3144,16 +3259,16 @@ describe( schemaId, className, propertyName, - 3, 0, + 1, ); const actualMetadataValue2 = pickMetadataAt( scene, schemaId, className, propertyName, - 6, 0, + 2, ); const expectedMetadataValue0 = 0.0; const expectedMetadataValue1 = 0.5; @@ -3174,6 +3289,165 @@ describe( scene.destroyForSpecs(); }); + it("picks normalized UINT8 SCALAR from a property texture with offset and scale in class property", async function () { + if (webglStub) { + return; + } + const schemaId = undefined; + const className = "exampleClass"; + const propertyName = "example_normalized_UINT8_SCALAR"; + const classPropertyOffset = 100.0; + const classPropertyScale = 2.0; + const metadataPropertyOffset = undefined; + const metadataPropertyScale = undefined; + const gltf = createPropertyTextureGltfNormalizedScalar( + classPropertyOffset, + classPropertyScale, + metadataPropertyOffset, + metadataPropertyScale, + ); + + const canvasSizeX = textureSizeX * canvasScaling; + const canvasSizeY = textureSizeY * canvasScaling; + const scene = createScene({ + canvas: createCanvas(canvasSizeX, canvasSizeY), + contextOptions: { + requestWebgl1: true, + }, + }); + + await loadAsModel(scene, gltf); + fitCameraToUnitSquare(scene.camera); + + scene.initializeFrame(); + scene.render(defaultDate); + + const actualMetadataValue0 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 0, + 0, + ); + const actualMetadataValue1 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 0, + 1, + ); + const actualMetadataValue2 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 0, + 2, + ); + const expectedMetadataValue0 = + classPropertyOffset + classPropertyScale * 0.0; + const expectedMetadataValue1 = + classPropertyOffset + classPropertyScale * 0.5; + const expectedMetadataValue2 = + classPropertyOffset + classPropertyScale * 1.0; + + expect(actualMetadataValue0).toEqualEpsilon( + expectedMetadataValue0, + propertyValueEpsilon, + ); + expect(actualMetadataValue1).toEqualEpsilon( + expectedMetadataValue1, + propertyValueEpsilon, + ); + expect(actualMetadataValue2).toEqualEpsilon( + expectedMetadataValue2, + propertyValueEpsilon, + ); + scene.destroyForSpecs(); + }); + + it("picks normalized UINT8 SCALAR from a property texture with offset and scale in property texture property", async function () { + if (webglStub) { + return; + } + const schemaId = undefined; + const className = "exampleClass"; + const propertyName = "example_normalized_UINT8_SCALAR"; + const classPropertyOffset = 100.0; + const classPropertyScale = 200.0; + // These should override the values from the class property: + const metadataPropertyOffset = 200.0; + const metadataPropertyScale = 3.0; + const gltf = createPropertyTextureGltfNormalizedScalar( + classPropertyOffset, + classPropertyScale, + metadataPropertyOffset, + metadataPropertyScale, + ); + + const canvasSizeX = textureSizeX * canvasScaling; + const canvasSizeY = textureSizeY * canvasScaling; + const scene = createScene({ + canvas: createCanvas(canvasSizeX, canvasSizeY), + contextOptions: { + requestWebgl1: true, + }, + }); + + await loadAsModel(scene, gltf); + fitCameraToUnitSquare(scene.camera); + + scene.initializeFrame(); + scene.render(defaultDate); + + const actualMetadataValue0 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 0, + 0, + ); + const actualMetadataValue1 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 0, + 1, + ); + const actualMetadataValue2 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 0, + 2, + ); + const expectedMetadataValue0 = + metadataPropertyOffset + metadataPropertyScale * 0.0; + const expectedMetadataValue1 = + metadataPropertyOffset + metadataPropertyScale * 0.5; + const expectedMetadataValue2 = + metadataPropertyOffset + metadataPropertyScale * 1.0; + + expect(actualMetadataValue0).toEqualEpsilon( + expectedMetadataValue0, + propertyValueEpsilon, + ); + expect(actualMetadataValue1).toEqualEpsilon( + expectedMetadataValue1, + propertyValueEpsilon, + ); + expect(actualMetadataValue2).toEqualEpsilon( + expectedMetadataValue2, + propertyValueEpsilon, + ); + scene.destroyForSpecs(); + }); + it("picks fixed length UINT8 SCALAR array from a property texture", async function () { if (webglStub) { return; @@ -3241,6 +3515,82 @@ describe( scene.destroyForSpecs(); }); + it("picks fixed length normalized UINT8 SCALAR array from a property texture", async function () { + if (webglStub) { + return; + } + const schemaId = undefined; + const className = "exampleClass"; + const propertyName = "example_fixed_length_normalized_UINT8_SCALAR_array"; + const classPropertyOffset = undefined; + const classPropertyScale = undefined; + const metadataPropertyOffset = undefined; + const metadataPropertyScale = undefined; + const gltf = createPropertyTextureGltfNormalizedScalarArray( + classPropertyOffset, + classPropertyScale, + metadataPropertyOffset, + metadataPropertyScale, + ); + + const canvasSizeX = textureSizeX * canvasScaling; + const canvasSizeY = textureSizeY * canvasScaling; + const scene = createScene({ + canvas: createCanvas(canvasSizeX, canvasSizeY), + contextOptions: { + requestWebgl1: true, + }, + }); + + await loadAsModel(scene, gltf); + fitCameraToUnitSquare(scene.camera); + + scene.initializeFrame(); + scene.render(defaultDate); + + const actualMetadataValue0 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 0, + 0, + ); + const actualMetadataValue1 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 1, + 1, + ); + const actualMetadataValue2 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 2, + 2, + ); + const expectedMetadataValue0 = [0, 0, 0]; + const expectedMetadataValue1 = [0.5, 0, 0.5]; + const expectedMetadataValue2 = [1.0, 0, 1.0]; + + expect(actualMetadataValue0).toEqualEpsilon( + expectedMetadataValue0, + propertyValueEpsilon, + ); + expect(actualMetadataValue1).toEqualEpsilon( + expectedMetadataValue1, + propertyValueEpsilon, + ); + expect(actualMetadataValue2).toEqualEpsilon( + expectedMetadataValue2, + propertyValueEpsilon, + ); + scene.destroyForSpecs(); + }); + it("picks UINT8 VEC2 from a property texture", async function () { if (webglStub) { return; @@ -3317,7 +3667,16 @@ describe( const schemaId = undefined; const className = "exampleClass"; const propertyName = "example_normalized_UINT8_VEC2"; - const gltf = createPropertyTextureGltfNormalizedVec2(); + const classPropertyOffset = undefined; + const classPropertyScale = undefined; + const metadataPropertyOffset = undefined; + const metadataPropertyScale = undefined; + const gltf = createPropertyTextureGltfNormalizedVec2( + classPropertyOffset, + classPropertyScale, + metadataPropertyOffset, + metadataPropertyScale, + ); const canvasSizeX = textureSizeX * canvasScaling; const canvasSizeY = textureSizeY * canvasScaling; @@ -3378,6 +3737,181 @@ describe( scene.destroyForSpecs(); }); + it("picks normalized UINT8 VEC2 from a property texture with offset and scale in class property", async function () { + if (webglStub) { + return; + } + + const schemaId = undefined; + const className = "exampleClass"; + const propertyName = "example_normalized_UINT8_VEC2"; + const classPropertyOffset = [100.0, 200.0]; + const classPropertyScale = [2.0, 3.0]; + const metadataPropertyOffset = undefined; + const metadataPropertyScale = undefined; + const gltf = createPropertyTextureGltfNormalizedVec2( + classPropertyOffset, + classPropertyScale, + metadataPropertyOffset, + metadataPropertyScale, + ); + + const canvasSizeX = textureSizeX * canvasScaling; + const canvasSizeY = textureSizeY * canvasScaling; + const scene = createScene({ + canvas: createCanvas(canvasSizeX, canvasSizeY), + contextOptions: { + requestWebgl1: true, + }, + }); + + await loadAsModel(scene, gltf); + fitCameraToUnitSquare(scene.camera); + + scene.initializeFrame(); + scene.render(defaultDate); + + const actualMetadataValue0 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 0, + 0, + ); + const actualMetadataValue1 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 1, + 1, + ); + const actualMetadataValue2 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 2, + 2, + ); + + const expectedMetadataValue0 = new Cartesian2( + classPropertyOffset[0] + classPropertyScale[0] * 0.0, + classPropertyOffset[1] + classPropertyScale[1] * 0.0, + ); + const expectedMetadataValue1 = new Cartesian2( + classPropertyOffset[0] + classPropertyScale[0] * 0.5, + classPropertyOffset[1] + classPropertyScale[1] * 0.0, + ); + const expectedMetadataValue2 = new Cartesian2( + classPropertyOffset[0] + classPropertyScale[0] * 1.0, + classPropertyOffset[1] + classPropertyScale[1] * 0.0, + ); + + expect(actualMetadataValue0).toEqualEpsilon( + expectedMetadataValue0, + propertyValueEpsilon, + ); + expect(actualMetadataValue1).toEqualEpsilon( + expectedMetadataValue1, + propertyValueEpsilon, + ); + expect(actualMetadataValue2).toEqualEpsilon( + expectedMetadataValue2, + propertyValueEpsilon, + ); + scene.destroyForSpecs(); + }); + + it("picks normalized UINT8 VEC2 from a property texture with offset and scale in property texture property", async function () { + if (webglStub) { + return; + } + + const schemaId = undefined; + const className = "exampleClass"; + const propertyName = "example_normalized_UINT8_VEC2"; + const classPropertyOffset = [100.0, 200.0]; + const classPropertyScale = [2.0, 3.0]; + // These should override the values from the class property: + const metadataPropertyOffset = [300.0, 400.0]; + const metadataPropertyScale = [4.0, 5.0]; + const gltf = createPropertyTextureGltfNormalizedVec2( + classPropertyOffset, + classPropertyScale, + metadataPropertyOffset, + metadataPropertyScale, + ); + + const canvasSizeX = textureSizeX * canvasScaling; + const canvasSizeY = textureSizeY * canvasScaling; + const scene = createScene({ + canvas: createCanvas(canvasSizeX, canvasSizeY), + contextOptions: { + requestWebgl1: true, + }, + }); + + await loadAsModel(scene, gltf); + fitCameraToUnitSquare(scene.camera); + + scene.initializeFrame(); + scene.render(defaultDate); + + const actualMetadataValue0 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 0, + 0, + ); + const actualMetadataValue1 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 1, + 1, + ); + const actualMetadataValue2 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 2, + 2, + ); + + const expectedMetadataValue0 = new Cartesian2( + metadataPropertyOffset[0] + metadataPropertyScale[0] * 0.0, + metadataPropertyOffset[1] + metadataPropertyScale[1] * 0.0, + ); + const expectedMetadataValue1 = new Cartesian2( + metadataPropertyOffset[0] + metadataPropertyScale[0] * 0.5, + metadataPropertyOffset[1] + metadataPropertyScale[1] * 0.0, + ); + const expectedMetadataValue2 = new Cartesian2( + metadataPropertyOffset[0] + metadataPropertyScale[0] * 1.0, + metadataPropertyOffset[1] + metadataPropertyScale[1] * 0.0, + ); + + expect(actualMetadataValue0).toEqualEpsilon( + expectedMetadataValue0, + propertyValueEpsilon, + ); + expect(actualMetadataValue1).toEqualEpsilon( + expectedMetadataValue1, + propertyValueEpsilon, + ); + expect(actualMetadataValue2).toEqualEpsilon( + expectedMetadataValue2, + propertyValueEpsilon, + ); + scene.destroyForSpecs(); + }); + it("picks UINT8 VEC3 from a property texture", async function () { if (webglStub) { return;