Updating draco decoder javascript API and other minor changes.

1. Javascript decoder is now exported as a module using DracoModule()
function that needs to be instantiated on the client.

2. Updated Javascript example applications and README.md

3. Added normalization function to VectorD

4. Added support for converting a single signed value to symbol for
entropy coding and vice versa

5. Minor code cleaning
This commit is contained in:
Ondrej Stava 2017-02-27 15:46:48 -08:00
parent 931a1de144
commit 127484be47
14 changed files with 126 additions and 61 deletions

View File

@ -56,6 +56,9 @@ ALL_C_OPTS += -O3
ALL_C_OPTS += -s ALLOW_MEMORY_GROWTH=1 ALL_C_OPTS += -s ALLOW_MEMORY_GROWTH=1
#ALL_C_OPTS += -s TOTAL_MEMORY=67108864 #ALL_C_OPTS += -s TOTAL_MEMORY=67108864
# Export the main module as "DracoModule".
ALL_C_OPTS += -s MODULARIZE=1 -s EXPORT_NAME="'DracoModule'"
# Do not create a .mem file. # Do not create a .mem file.
ALL_C_OPTS += --memory-init-file 0 ALL_C_OPTS += --memory-init-file 0

View File

@ -319,28 +319,30 @@ Javascript Decoder API
The Javascript decoder is located in `javascript/draco_decoder.js`. The The Javascript decoder is located in `javascript/draco_decoder.js`. The
Javascript decoder can decode mesh and point cloud. In order to use the Javascript decoder can decode mesh and point cloud. In order to use the
decoder you must first create `DecoderBuffer` and `WebIDLWrapper` objects. Set decoder, you must first create an instance of `DracoModule`. The instance is
then used to create `DecoderBuffer` and `WebIDLWrapper` objects. Set
the encoded data in the `DecoderBuffer`. Then call `GetEncodedGeometryType()` the encoded data in the `DecoderBuffer`. Then call `GetEncodedGeometryType()`
to identify the type of geometry, e.g. mesh or point cloud. Then call either to identify the type of geometry, e.g. mesh or point cloud. Then call either
`DecodeMeshFromBuffer()` or `DecodePointCloudFromBuffer()`, which will return `DecodeMeshFromBuffer()` or `DecodePointCloudFromBuffer()`, which will return
a Mesh object or a point cloud. For example: a Mesh object or a point cloud. For example:
~~~~~ js ~~~~~ js
const buffer = new Module.DecoderBuffer(); const dracoDecoder = DracoModule();
const buffer = new dracoDecoder.DecoderBuffer();
buffer.Init(encFileData, encFileData.length); buffer.Init(encFileData, encFileData.length);
const wrapper = new Module.WebIDLWrapper(); const wrapper = new dracoDecoder.WebIDLWrapper();
const geometryType = wrapper.GetEncodedGeometryType(buffer); const geometryType = wrapper.GetEncodedGeometryType(buffer);
let outputGeometry; let outputGeometry;
if (geometryType == Module.TRIANGULAR_MESH) { if (geometryType == dracoDecoder.TRIANGULAR_MESH) {
outputGeometry = wrapper.DecodeMeshFromBuffer(buffer); outputGeometry = wrapper.DecodeMeshFromBuffer(buffer);
} else { } else {
outputGeometry = wrapper.DecodePointCloudFromBuffer(buffer); outputGeometry = wrapper.DecodePointCloudFromBuffer(buffer);
} }
Module.destroy(outputGeometry); dracoDecoder.destroy(outputGeometry);
Module.destroy(wrapper); dracoDecoder.destroy(wrapper);
Module.destroy(buffer); dracoDecoder.destroy(buffer);
~~~~~ ~~~~~
Please see `javascript/emscripten/draco_web.idl` for the full API. Please see `javascript/emscripten/draco_web.idl` for the full API.

View File

@ -25,16 +25,19 @@ namespace draco {
void ConvertSymbolsToSignedInts(const uint32_t *in, int in_values, void ConvertSymbolsToSignedInts(const uint32_t *in, int in_values,
int32_t *out) { int32_t *out) {
for (int i = 0; i < in_values; ++i) { for (int i = 0; i < in_values; ++i) {
uint32_t val = in[i]; out[i] = ConvertSymbolToSignedInt(in[i]);
const bool is_negative = (val & 1);
val >>= 1;
int32_t ret = static_cast<int32_t>(val);
if (is_negative)
ret = -ret - 1;
out[i] = ret;
} }
} }
int32_t ConvertSymbolToSignedInt(uint32_t val) {
const bool is_negative = (val & 1);
val >>= 1;
int32_t ret = static_cast<int32_t>(val);
if (is_negative)
ret = -ret - 1;
return ret;
}
template <template <int> class SymbolDecoderT> template <template <int> class SymbolDecoderT>
bool DecodeTaggedSymbols(int num_values, int num_components, bool DecodeTaggedSymbols(int num_values, int num_components,
DecoderBuffer *src_buffer, uint32_t *out_values); DecoderBuffer *src_buffer, uint32_t *out_values);

View File

@ -24,6 +24,10 @@ namespace draco {
void ConvertSymbolsToSignedInts(const uint32_t *in, int in_values, void ConvertSymbolsToSignedInts(const uint32_t *in, int in_values,
int32_t *out); int32_t *out);
// Converts a single unsigned integer symbol encoded with an entropy encoder
// back to a signed value.
int32_t ConvertSymbolToSignedInt(uint32_t val);
// Decodes an array of symbols that was previously encoded with an entropy code. // Decodes an array of symbols that was previously encoded with an entropy code.
// Returns false on error. // Returns false on error.
bool DecodeSymbols(int num_values, int num_components, bool DecodeSymbols(int num_values, int num_components,

View File

@ -33,17 +33,20 @@ void ConvertSignedIntsToSymbols(const int32_t *in, int in_values,
// encoding. // encoding.
// Put the sign bit into LSB pos and shift the rest one bit left. // Put the sign bit into LSB pos and shift the rest one bit left.
for (int i = 0; i < in_values; ++i) { for (int i = 0; i < in_values; ++i) {
int32_t val = in[i]; out[i] = ConvertSignedIntToSymbol(in[i]);
const bool is_negative = (val < 0);
if (is_negative)
val = -val - 1; // Map -1 to 0, -2 to -1, etc..
val <<= 1;
if (is_negative)
val |= 1;
out[i] = static_cast<uint32_t>(val);
} }
} }
uint32_t ConvertSignedIntToSymbol(int32_t val) {
const bool is_negative = (val < 0);
if (is_negative)
val = -val - 1; // Map -1 to 0, -2 to -1, etc..
val <<= 1;
if (is_negative)
val |= 1;
return static_cast<uint32_t>(val);
}
// Computes bit lengths of the input values. If num_components > 1, the values // Computes bit lengths of the input values. If num_components > 1, the values
// are processed in "num_components" sized chunks and the bit length is always // are processed in "num_components" sized chunks and the bit length is always
// computed for the largest value from the chunk. // computed for the largest value from the chunk.

View File

@ -24,6 +24,10 @@ namespace draco {
void ConvertSignedIntsToSymbols(const int32_t *in, int in_values, void ConvertSignedIntsToSymbols(const int32_t *in, int in_values,
uint32_t *out); uint32_t *out);
// Helper function that converts a single signed integer value into an unsigned
// integer symbol that can be encoded using an entropy encoder.
uint32_t ConvertSignedIntToSymbol(int32_t val);
// Encodes an array of symbols using an entropy coding. This function // Encodes an array of symbols using an entropy coding. This function
// automatically decides whether to encode the symbol values using using bit // automatically decides whether to encode the symbol values using using bit
// length tags (see EncodeTaggedSymbols), or whether to encode them directly // length tags (see EncodeTaggedSymbols), or whether to encode them directly

View File

@ -16,6 +16,7 @@
#define DRACO_CORE_VECTOR_D_H_ #define DRACO_CORE_VECTOR_D_H_
#include <inttypes.h> #include <inttypes.h>
#include <algorithm>
#include <array> #include <array>
#include "core/macros.h" #include "core/macros.h"
@ -149,6 +150,15 @@ class VectorD {
} }
return ret; return ret;
} }
void Normalize() {
const CoeffT magnitude = sqrt(this->SquaredNorm());
if (magnitude == 0) {
return;
}
for (int i = 0; i < dimension_t; ++i) {
(*this)[i] /= magnitude;
}
}
CoeffT *data() { return &(v_[0]); } CoeffT *data() { return &(v_[0]); }
private: private:

View File

@ -47,7 +47,7 @@ TEST_F(VectorDTest, TestOperators) {
ASSERT_EQ(v[1], 0); ASSERT_EQ(v[1], 0);
ASSERT_EQ(v[2], 0); ASSERT_EQ(v[2], 0);
} }
const Vector3f v(1, 2, 3); Vector3f v(1, 2, 3);
ASSERT_EQ(v[0], 1); ASSERT_EQ(v[0], 1);
ASSERT_EQ(v[1], 2); ASSERT_EQ(v[1], 2);
ASSERT_EQ(v[2], 3); ASSERT_EQ(v[2], 3);
@ -83,6 +83,25 @@ TEST_F(VectorDTest, TestOperators) {
ASSERT_EQ(v.SquaredNorm(), 14); ASSERT_EQ(v.SquaredNorm(), 14);
ASSERT_EQ(v.Dot(v), 14); ASSERT_EQ(v.Dot(v), 14);
Vector3f new_v = v;
new_v.Normalize();
const float eps = 0.001;
const float magnitude = sqrt(v.SquaredNorm());
const float new_magnitude = sqrt(new_v.SquaredNorm());
ASSERT_LE(new_magnitude, 1 + eps);
ASSERT_GE(new_magnitude, 1 - eps);
for (int i = 0; i < 3; ++i) {
new_v[i] *= magnitude;
ASSERT_LE(new_v[i], v[i] + eps);
ASSERT_GE(new_v[i], v[i] - eps);
}
Vector3f x(0, 0, 0);
x.Normalize();
for (int i = 0; i < 3; ++i) {
ASSERT_EQ(0, x[i]);
}
} }
TEST_F(VectorDTest, TestSquaredDistance) { TEST_F(VectorDTest, TestSquaredDistance) {

File diff suppressed because one or more lines are too long

View File

@ -44,17 +44,17 @@ THREE.DRACOLoader.prototype = {
/* /*
* Here is how to use Draco Javascript decoder and get the geometry. * Here is how to use Draco Javascript decoder and get the geometry.
*/ */
const buffer = new DracoModule.DecoderBuffer(); const buffer = new dracoDecoder.DecoderBuffer();
buffer.Init(new Int8Array(rawBuffer), rawBuffer.byteLength); buffer.Init(new Int8Array(rawBuffer), rawBuffer.byteLength);
const wrapper = new DracoModule.WebIDLWrapper(); const wrapper = new dracoDecoder.WebIDLWrapper();
/* /*
* Determine what type is this file, mesh or point cloud. * Determine what type is this file: mesh or point cloud.
*/ */
const geometryType = wrapper.GetEncodedGeometryType(buffer); const geometryType = wrapper.GetEncodedGeometryType(buffer);
if (geometryType == DracoModule.TRIANGULAR_MESH) { if (geometryType == dracoDecoder.TRIANGULAR_MESH) {
fileDisplayArea.innerText = "Loaded a mesh.\n"; fileDisplayArea.innerText = "Loaded a mesh.\n";
} else if (geometryType == DracoModule.POINT_CLOUD) { } else if (geometryType == dracoDecoder.POINT_CLOUD) {
fileDisplayArea.innerText = "Loaded a point cloud.\n"; fileDisplayArea.innerText = "Loaded a point cloud.\n";
} else { } else {
const errorMsg = "Error: Unknown geometry type."; const errorMsg = "Error: Unknown geometry type.";
@ -67,13 +67,13 @@ THREE.DRACOLoader.prototype = {
convertDracoGeometryTo3JS: function(wrapper, geometryType, buffer) { convertDracoGeometryTo3JS: function(wrapper, geometryType, buffer) {
let dracoGeometry; let dracoGeometry;
const start_time = performance.now(); const start_time = performance.now();
if (geometryType == DracoModule.TRIANGULAR_MESH) { if (geometryType == dracoDecoder.TRIANGULAR_MESH) {
dracoGeometry = wrapper.DecodeMeshFromBuffer(buffer); dracoGeometry = wrapper.DecodeMeshFromBuffer(buffer);
} else { } else {
dracoGeometry = wrapper.DecodePointCloudFromBuffer(buffer); dracoGeometry = wrapper.DecodePointCloudFromBuffer(buffer);
} }
const decode_end = performance.now(); const decode_end = performance.now();
DracoModule.destroy(buffer); dracoDecoder.destroy(buffer);
/* /*
* Example on how to retrieve mesh and attributes. * Example on how to retrieve mesh and attributes.
*/ */
@ -81,7 +81,7 @@ THREE.DRACOLoader.prototype = {
let numVertexCoordinates, numTextureCoordinates, numAttributes; let numVertexCoordinates, numTextureCoordinates, numAttributes;
// For output basic geometry information. // For output basic geometry information.
let geometryInfoStr; let geometryInfoStr;
if (geometryType == DracoModule.TRIANGULAR_MESH) { if (geometryType == dracoDecoder.TRIANGULAR_MESH) {
numFaces = dracoGeometry.num_faces(); numFaces = dracoGeometry.num_faces();
geometryInfoStr += "Number of faces loaded: " + numFaces.toString() geometryInfoStr += "Number of faces loaded: " + numFaces.toString()
+ ".\n"; + ".\n";
@ -99,50 +99,51 @@ THREE.DRACOLoader.prototype = {
// Get position attribute. Must exists. // Get position attribute. Must exists.
const posAttId = wrapper.GetAttributeId(dracoGeometry, const posAttId = wrapper.GetAttributeId(dracoGeometry,
Module.POSITION); dracoDecoder.POSITION);
if (posAttId == -1) { if (posAttId == -1) {
const errorMsg = "No position attribute found in the mesh."; const errorMsg = "No position attribute found in the mesh.";
fileDisplayArea.innerText = errorMsg; fileDisplayArea.innerText = errorMsg;
DracoModule.destroy(wrapper); dracoDecoder.destroy(wrapper);
DracoModule.destroy(dracoGeometry); dracoDecoder.destroy(dracoGeometry);
throw new Error(errorMsg); throw new Error(errorMsg);
} }
const posAttribute = wrapper.GetAttribute(dracoGeometry, posAttId); const posAttribute = wrapper.GetAttribute(dracoGeometry, posAttId);
const posAttributeData = new DracoModule.DracoFloat32Array(); const posAttributeData = new dracoDecoder.DracoFloat32Array();
wrapper.GetAttributeFloatForAllPoints( wrapper.GetAttributeFloatForAllPoints(
dracoGeometry, posAttribute, posAttributeData); dracoGeometry, posAttribute, posAttributeData);
// Get color attributes if exists. // Get color attributes if exists.
const colorAttId = wrapper.GetAttributeId(dracoGeometry, Module.COLOR); const colorAttId = wrapper.GetAttributeId(dracoGeometry,
dracoDecoder.COLOR);
let colAttributeData; let colAttributeData;
if (colorAttId != -1) { if (colorAttId != -1) {
geometryInfoStr += "\nLoaded color attribute.\n"; geometryInfoStr += "\nLoaded color attribute.\n";
const colAttribute = wrapper.GetAttribute(dracoGeometry, colorAttId); const colAttribute = wrapper.GetAttribute(dracoGeometry, colorAttId);
colAttributeData = new DracoModule.DracoFloat32Array(); colAttributeData = new dracoDecoder.DracoFloat32Array();
wrapper.GetAttributeFloatForAllPoints(dracoGeometry, colAttribute, wrapper.GetAttributeFloatForAllPoints(dracoGeometry, colAttribute,
colAttributeData); colAttributeData);
} }
// Get normal attributes if exists. // Get normal attributes if exists.
const normalAttId = const normalAttId =
wrapper.GetAttributeId(dracoGeometry, Module.NORMAL); wrapper.GetAttributeId(dracoGeometry, dracoDecoder.NORMAL);
let norAttributeData; let norAttributeData;
if (normalAttId != -1) { if (normalAttId != -1) {
geometryInfoStr += "\nLoaded normal attribute.\n"; geometryInfoStr += "\nLoaded normal attribute.\n";
const norAttribute = wrapper.GetAttribute(dracoGeometry, normalAttId); const norAttribute = wrapper.GetAttribute(dracoGeometry, normalAttId);
norAttributeData = new DracoModule.DracoFloat32Array(); norAttributeData = new dracoDecoder.DracoFloat32Array();
wrapper.GetAttributeFloatForAllPoints(dracoGeometry, norAttribute, wrapper.GetAttributeFloatForAllPoints(dracoGeometry, norAttribute,
norAttributeData); norAttributeData);
} }
// Get texture coord attributes if exists. // Get texture coord attributes if exists.
const texCoordAttId = const texCoordAttId =
wrapper.GetAttributeId(dracoGeometry, Module.TEX_COORD); wrapper.GetAttributeId(dracoGeometry, dracoDecoder.TEX_COORD);
let textCoordAttributeData; let textCoordAttributeData;
if (texCoordAttId != -1) { if (texCoordAttId != -1) {
geometryInfoStr += "\nLoaded texture coordinate attribute.\n"; geometryInfoStr += "\nLoaded texture coordinate attribute.\n";
const texCoordAttribute = wrapper.GetAttribute(dracoGeometry, const texCoordAttribute = wrapper.GetAttribute(dracoGeometry,
texCoordAttId); texCoordAttId);
textCoordAttributeData = new DracoModule.DracoFloat32Array(); textCoordAttributeData = new dracoDecoder.DracoFloat32Array();
wrapper.GetAttributeFloatForAllPoints(dracoGeometry, wrapper.GetAttributeFloatForAllPoints(dracoGeometry,
texCoordAttribute, texCoordAttribute,
textCoordAttributeData); textCoordAttributeData);
@ -189,17 +190,17 @@ THREE.DRACOLoader.prototype = {
} }
} }
DracoModule.destroy(posAttributeData); dracoDecoder.destroy(posAttributeData);
if (colorAttId != -1) if (colorAttId != -1)
DracoModule.destroy(colAttributeData); dracoDecoder.destroy(colAttributeData);
if (normalAttId != -1) if (normalAttId != -1)
DracoModule.destroy(norAttributeData); dracoDecoder.destroy(norAttributeData);
if (texCoordAttId != -1) if (texCoordAttId != -1)
DracoModule.destroy(textCoordAttributeData); dracoDecoder.destroy(textCoordAttributeData);
// For mesh, we need to generate the faces. // For mesh, we need to generate the faces.
if (geometryType == DracoModule.TRIANGULAR_MESH) { if (geometryType == dracoDecoder.TRIANGULAR_MESH) {
const ia = new DracoInt32Array(); const ia = new dracoDecoder.DracoInt32Array();
for (let i = 0; i < numFaces; ++i) { for (let i = 0; i < numFaces; ++i) {
wrapper.GetFaceFromMesh(dracoGeometry, i, ia); wrapper.GetFaceFromMesh(dracoGeometry, i, ia);
const index = i * 3; const index = i * 3;
@ -207,16 +208,16 @@ THREE.DRACOLoader.prototype = {
geometryBuffer.indices[index + 1] = ia.GetValue(1); geometryBuffer.indices[index + 1] = ia.GetValue(1);
geometryBuffer.indices[index + 2] = ia.GetValue(2); geometryBuffer.indices[index + 2] = ia.GetValue(2);
} }
DracoModule.destroy(ia); dracoDecoder.destroy(ia);
} }
DracoModule.destroy(wrapper); dracoDecoder.destroy(wrapper);
DracoModule.destroy(dracoGeometry); dracoDecoder.destroy(dracoGeometry);
fileDisplayArea.innerText += geometryInfoStr; fileDisplayArea.innerText += geometryInfoStr;
// Import data to Three JS geometry. // Import data to Three JS geometry.
const geometry = new THREE.BufferGeometry(); const geometry = new THREE.BufferGeometry();
if (geometryType == DracoModule.TRIANGULAR_MESH) { if (geometryType == dracoDecoder.TRIANGULAR_MESH) {
geometry.setIndex(new(geometryBuffer.indices.length > 65535 ? geometry.setIndex(new(geometryBuffer.indices.length > 65535 ?
THREE.Uint32BufferAttribute : THREE.Uint16BufferAttribute) THREE.Uint32BufferAttribute : THREE.Uint16BufferAttribute)
(geometryBuffer.indices, 1)); (geometryBuffer.indices, 1));

View File

@ -47,8 +47,7 @@
<script> <script>
'use strict'; 'use strict';
// Module that exposes all the core funcionality of the Draco decoder. const dracoDecoder = DracoModule();
const DracoModule = Module;
let container; let container;
let camera, cameraTarget, scene, renderer; let camera, cameraTarget, scene, renderer;

View File

@ -9,6 +9,7 @@
// String to hold table output. // String to hold table output.
let dt = ''; let dt = '';
const dracoDecoder = DracoModule();
function startTable() { function startTable() {
dt += '<table><tr>'; dt += '<table><tr>';
@ -62,15 +63,15 @@ function TestMeshDecodingAsync(filenameList, index) {
const total_t0 = performance.now(); const total_t0 = performance.now();
const buffer = new Module.DecoderBuffer(); const buffer = new dracoDecoder.DecoderBuffer();
buffer.Init(byteArray, byteArray.length); buffer.Init(byteArray, byteArray.length);
const wrapper = new Module.WebIDLWrapper(); const wrapper = new dracoDecoder.WebIDLWrapper();
const decode_t0 = performance.now(); const decode_t0 = performance.now();
const geometryType = wrapper.GetEncodedGeometryType(buffer); const geometryType = wrapper.GetEncodedGeometryType(buffer);
let outputGeometry; let outputGeometry;
if (geometryType == Module.TRIANGULAR_MESH) { if (geometryType == dracoDecoder.TRIANGULAR_MESH) {
outputGeometry = wrapper.DecodeMeshFromBuffer(buffer); outputGeometry = wrapper.DecodeMeshFromBuffer(buffer);
} else { } else {
outputGeometry = wrapper.DecodePointCloudFromBuffer(buffer); outputGeometry = wrapper.DecodePointCloudFromBuffer(buffer);
@ -83,9 +84,9 @@ function TestMeshDecodingAsync(filenameList, index) {
addCell('' + byteArray.length, false); addCell('' + byteArray.length, false);
addCell('' + outputGeometry.num_points(), false); addCell('' + outputGeometry.num_points(), false);
Module.destroy(outputGeometry); dracoDecoder.destroy(outputGeometry);
Module.destroy(wrapper); dracoDecoder.destroy(wrapper);
Module.destroy(buffer); dracoDecoder.destroy(buffer);
if (index < filenameList.length - 1) { if (index < filenameList.length - 1) {
index = index + 1; index = index + 1;

View File

@ -80,6 +80,7 @@ template <class TraversalProcessorT, class TraversalObserverT,
class EdgeBreakerObserverT = EdgeBreakerObserver> class EdgeBreakerObserverT = EdgeBreakerObserver>
class EdgeBreakerTraverser { class EdgeBreakerTraverser {
public: public:
typedef TraversalProcessorT TraversalProcessor;
typedef typename TraversalProcessorT::CornerTable CornerTable; typedef typename TraversalProcessorT::CornerTable CornerTable;
EdgeBreakerTraverser() {} EdgeBreakerTraverser() {}
@ -98,6 +99,13 @@ class EdgeBreakerTraverser {
Init(processor, traversal_observer); Init(processor, traversal_observer);
edgebreaker_observer_ = edgebreaker_observer; edgebreaker_observer_ = edgebreaker_observer;
} }
// Called before any traversing starts.
void OnTraversalStart() {}
// Called when all the traversing is done.
void OnTraversalEnd() {}
void TraverseFromCorner(CornerIndex corner_id) { void TraverseFromCorner(CornerIndex corner_id) {
if (processor_.IsFaceVisited(corner_id)) if (processor_.IsFaceVisited(corner_id))
return; // Already traversed. return; // Already traversed.
@ -198,6 +206,9 @@ class EdgeBreakerTraverser {
const CornerTable *corner_table() const { return corner_table_; } const CornerTable *corner_table() const { return corner_table_; }
const TraversalProcessorT &traversal_processor() const { return processor_; } const TraversalProcessorT &traversal_processor() const { return processor_; }
const TraversalObserverT &traversal_observer() const {
return traversal_observer_;
}
private: private:
const CornerTable *corner_table_; const CornerTable *corner_table_;

View File

@ -101,7 +101,6 @@ class Mesh : public PointCloud {
// that converts vertex indices into attribute indices. // that converts vertex indices into attribute indices.
IndexTypeVector<FaceIndex, Face> faces_; IndexTypeVector<FaceIndex, Face> faces_;
friend class MeshBuilder;
friend struct MeshHasher; friend struct MeshHasher;
}; };