QOpenGLWidget自定义控件— 2D点云显示(支持平移、放缩、绘制网格)
自定义QOpenGLWidget控件实现2D点云(XOZ平面内的点)绘制,支持
(1)放缩;
(2)平移;
(3)绘制网格;
PointCloudViewer.h
#ifndef POINTCLOUDVIEWER_H
#define POINTCLOUDVIEWER_H#include <QOpenGLWidget>
#include <QOpenGLFunctions>
#include <QOpenGLShaderProgram>
#include <QOpenGLBuffer>
#include <QVector3D>
#include <QMatrix4x4>
#include <QMouseEvent>
#include <QWheelEvent>
#include <vector>
#include<glm/glm.hpp>
#include <QOpenGLVertexArrayObject>
#include<MouseManager.h>class PointCloudViewer : public QOpenGLWidget, protected QOpenGLFunctions
{Q_OBJECTpublic:explicit PointCloudViewer(QWidget* parent = nullptr);~PointCloudViewer();void setPointCloud(const std::vector<QVector3D>& points);void setGridSpacing(float spacing);void setGridColor(const QColor& color);void setPointSize(float size);void setPointColor(const QColor& color);protected:void initializeGL() override;void resizeGL(int w, int h) override;void paintGL() override;void mousePressEvent(QMouseEvent* event) override;void mouseReleaseEvent(QMouseEvent* event) override;void mouseMoveEvent(QMouseEvent* event) override;void wheelEvent(QWheelEvent* event) override;private:void initShaders();void updateProjection();void updateView();void calculateBoundingBox();QVector2D worldToScreen(const QVector3D& worldPos) const;QVector3D screenToWorld(const QVector2D& screenPos, float y = 0.0f) const;void printContextInformation();void drawGrids();bool isTranslating() const;float window_ratio = 1.0;bool rotating = false;bool translating = false;bool selecting = false;float left_plane = 0; // = -1000.0f * SCR_RATIO;float right_plane = 0;// = 1000.0f * SCR_RATIO;float top_plane = 1000.0f;float bottom_plane = -1000.0f;float near_plane = 0.1;float far_plane = 10.0f;float x_span = 0;float y_span = 0;float z_span = 0;ViewPort view_whole;MouseManager mouse_manager;// OpenGL resourcesQOpenGLShaderProgram m_pointProgram;QOpenGLShaderProgram m_gridProgram;QOpenGLBuffer m_pointBuffer;QOpenGLBuffer m_gridBuffer;// Datastd::vector<QVector3D> m_points;QVector3D m_minBound;QVector3D m_maxBound;float m_gridSpacing;// Appearancefloat m_pointSize;QColor m_pointColor;QColor m_gridColor;// View controlQMatrix4x4 m_projection;QMatrix4x4 m_view;QVector3D m_translation;float m_scale;QPoint m_lastMousePos;bool m_dragging;
};#endif // POINTCLOUDVIEWER_H
PointCloudViewer.cpp
#include "PointCloudViewer.h"
#include <QOpenGLContext>
#include <QDebug>
#include <QOpenGLShader>
#include <QPainter>
#include <cmath>static const GLchar* vertexShader_point = "#version 330 core\n"
"layout(location = 0) in vec3 position;\n"
"uniform mat4 mvp_matrix;\n"
"uniform float pointSize;\n"
"void main()\n"
"{\n"
" gl_Position = mvp_matrix * vec4(position, 1.0);\n"
" gl_PointSize = pointSize;\n"
"}\n";static const GLchar* vertexShader_grid="#version 330 core\n"
"layout(location = 0) in vec3 position;\n"
"uniform mat4 mvp_matrix;\n"
"void main()\n"
"{\n"
" gl_Position = mvp_matrix * vec4(position, 1.0);\n"
"}\n";static const GLchar* fragmentShader_common = "#version 330 core\n"
"uniform vec4 color;\n"
"out vec4 fragColor;\n"
"void main()\n"
"{\n"
" fragColor = color;\n"
"}\n";inline void GLClearError() {while (glGetError() != GL_NO_ERROR);
}inline bool GLLogCall(const char* function, const char* file, int line) {GLenum error;while (error = glGetError()) {QString errorStr;switch (error) {case GL_INVALID_ENUM: errorStr = "GL_INVALID_ENUM"; break;case GL_INVALID_VALUE: errorStr = "GL_INVALID_VALUE"; break;case GL_INVALID_OPERATION: errorStr = "GL_INVALID_OPERATION"; break;case GL_STACK_OVERFLOW: errorStr = "GL_STACK_OVERFLOW"; break;case GL_STACK_UNDERFLOW: errorStr = "GL_STACK_UNDERFLOW"; break;case GL_OUT_OF_MEMORY: errorStr = "GL_OUT_OF_MEMORY"; break;case GL_INVALID_FRAMEBUFFER_OPERATION: errorStr = "GL_INVALID_FRAMEBUFFER_OPERATION"; break;default: errorStr = QString::number(error); break;}qCritical() << "[OpenGL Error] " << errorStr<< " in " << function<< " (" << file << ":" << line << ")";return false;}return true;
}#define GL_CALL(x) \GLClearError(); \x; \if(!GLLogCall(#x, __FILE__, __LINE__)) { \qFatal("OpenGL error detected, aborting"); \}PointCloudViewer::PointCloudViewer(QWidget* parent): QOpenGLWidget(parent),m_gridSpacing(1.0f),m_pointSize(3.0f),m_pointColor(Qt::blue),m_gridColor(Qt::lightGray),m_translation(0, 0, 0),m_scale(1.0f),m_dragging(false)
{setFocusPolicy(Qt::StrongFocus);//QSurfaceFormat format;//format.setVersion(3, 3);//format.setProfile(QSurfaceFormat::CoreProfile);//format.setDepthBufferSize(24);//QSurfaceFormat::setDefaultFormat(format);
}PointCloudViewer::~PointCloudViewer()
{makeCurrent();m_pointBuffer.destroy();m_gridBuffer.destroy();doneCurrent();
}void PointCloudViewer::setPointCloud(const std::vector<QVector3D>& points)
{m_points = points;calculateBoundingBox();update();
}void PointCloudViewer::setGridSpacing(float spacing)
{m_gridSpacing = spacing;update();
}void PointCloudViewer::setGridColor(const QColor& color)
{m_gridColor = color;update();
}void PointCloudViewer::setPointSize(float size)
{m_pointSize = size;update();
}void PointCloudViewer::setPointColor(const QColor& color)
{m_pointColor = color;update();
}// 打印相关信息,调试用
void PointCloudViewer::printContextInformation()
{QString glType;QString glVersion;QString glProfile;// 获取版本信息glType = (context()->isOpenGLES()) ? "OpenGL ES" : "OpenGL";glVersion = reinterpret_cast<const char*>(glGetString(GL_VERSION));// 获取 Profile 信息
#define CASE(c) \case QSurfaceFormat::c: \glProfile = #c; \breakswitch (format().profile()) {CASE(NoProfile);CASE(CoreProfile);CASE(CompatibilityProfile);}
#undef CASEqDebug() << qPrintable(glType) << qPrintable(glVersion) << "(" << qPrintable(glProfile) << ")";
}void PointCloudViewer::drawGrids()
{// Draw gridif (!m_pointProgram.bind()) {qWarning() << "Failed to bind point program";}m_gridProgram.setUniformValue("mvp_matrix", m_projection * m_view);m_gridProgram.setUniformValue("color", m_gridColor);// Generate grid linesfloat left = m_minBound.x() - 1.0f;float right = m_maxBound.x() + 1.0f;float bottom = m_minBound.z() - 1.0f;float top = m_maxBound.z() + 1.0f;// Calculate visible grid range based on current viewQVector3D worldBottomLeft = screenToWorld(QVector2D(0, height()));QVector3D worldTopRight = screenToWorld(QVector2D(width(), 0));left = worldBottomLeft.x();right = worldTopRight.x();bottom = worldBottomLeft.z();top = worldTopRight.z();// Adjust grid spacing based on zoom levelfloat dynamicGridSpacing = m_gridSpacing;float minSpacing = 0.1f;float maxSpacing = 100.0f;// Find appropriate grid spacingfloat viewWidth = right - left;float targetGridCount = 10.0f; // Aim for about 10 grid lines visibledynamicGridSpacing = viewWidth / targetGridCount;// Round to nearest power of 10 multiplied by 1, 2 or 5float exponent = floor(log10(dynamicGridSpacing));float fraction = dynamicGridSpacing / pow(10.0f, exponent);if (fraction < 1.5f) {dynamicGridSpacing = pow(10.0f, exponent);}else if (fraction < 3.0f) {dynamicGridSpacing = 2.0f * pow(10.0f, exponent);}else if (fraction < 7.0f) {dynamicGridSpacing = 5.0f * pow(10.0f, exponent);}else {dynamicGridSpacing = 10.0f * pow(10.0f, exponent);}dynamicGridSpacing = qBound(minSpacing, dynamicGridSpacing, maxSpacing);// Generate grid linesstd::vector<QVector3D> gridLines;// Vertical linesfloat startX = floor(left / dynamicGridSpacing) * dynamicGridSpacing;for (float x = startX; x <= right; x += dynamicGridSpacing) {gridLines.emplace_back(x, 0.0f, bottom);gridLines.emplace_back(x, 0.0f, top);}// Horizontal linesfloat startZ = floor(bottom / dynamicGridSpacing) * dynamicGridSpacing;for (float z = startZ; z <= top; z += dynamicGridSpacing) {gridLines.emplace_back(left, 0.0f, z);gridLines.emplace_back(right, 0.0f, z);}// Update grid bufferm_gridBuffer.bind();m_gridBuffer.allocate(gridLines.data(), gridLines.size() * sizeof(QVector3D));m_gridBuffer.release();glLineWidth(1.0f);m_gridBuffer.bind();m_gridProgram.enableAttributeArray(0);m_gridProgram.setAttributeBuffer(0, GL_FLOAT, 0, 3, sizeof(QVector3D));glDrawArrays(GL_LINES, 0, gridLines.size());m_gridBuffer.release();m_gridProgram.release();// Draw scale markers using QPainterQPainter painter(this);painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);painter.setPen(QPen(m_gridColor, 1));// Calculate scale factor (world units per pixel)float scaleFactor = (right - left) / width();// Draw horizontal scale (top)float textHeight = painter.fontMetrics().height();float markerLength = 5.0f;float startMarkerX = floor(left / dynamicGridSpacing) * dynamicGridSpacing;for (float x = startMarkerX; x <= right; x += dynamicGridSpacing) {QVector2D screenPos = worldToScreen(QVector3D(x, 0.0f, top));painter.drawLine(screenPos.x(), 0, screenPos.x(), markerLength);QString label = QString::number(x, 'f', dynamicGridSpacing < 1.0f ? 2 : 1);painter.drawText(screenPos.x() - 20, markerLength + textHeight, 40, textHeight,Qt::AlignCenter, label);}// Draw vertical scale (left)float startMarkerZ = floor(bottom / dynamicGridSpacing) * dynamicGridSpacing;for (float z = startMarkerZ; z <= top; z += dynamicGridSpacing) {QVector2D screenPos = worldToScreen(QVector3D(left, 0.0f, z));painter.drawLine(0, screenPos.y(), markerLength, screenPos.y());QString label = QString::number(z, 'f', dynamicGridSpacing < 1.0f ? 2 : 1);painter.drawText(markerLength + 2,screenPos.y() - textHeight / 2,40, textHeight, Qt::AlignLeft | Qt::AlignVCenter, label);}painter.end();
}void PointCloudViewer::initializeGL()
{initializeOpenGLFunctions();makeCurrent();printContextInformation();QSurfaceFormat format;format.setSamples(4);//启用4x多重采样this->setFormat(format);GL_CALL(glClearColor(1.0f, 1.0f, 1.0f, 1.0f));glClearColor(0.5f, 0.5f, 0.5f, 1.0f); // 背景色glClearDepth(1.0); // 深度缓存initShaders();// Initialize point bufferm_pointBuffer.create();if (!m_points.empty()) {m_pointBuffer.bind();m_pointBuffer.allocate(m_points.data(), m_points.size() * sizeof(QVector3D));m_pointBuffer.release();}// Grid buffer will be updated in paintGLm_gridBuffer.create();qDebug() << "Renderer:" << (const char*)glGetString(GL_RENDERER);glDisable(GL_DEPTH_TEST); // 2D绘制通常不需要深度测试glEnable(GL_PROGRAM_POINT_SIZE); // 启用可编程点大小
}void PointCloudViewer::resizeGL(int w, int h)
{view_whole.init(0, 0, w, h);mouse_manager.setViewPort(&view_whole);mouse_manager.setScreenSize(w, h);updateProjection();
}void PointCloudViewer::paintGL()
{glViewport(0, 0, width(), height());glBindFramebuffer(GL_FRAMEBUFFER, 0);glViewport(0, 0, width(), height());//GL_CALL(glClearColor(1.0f, 0.0f, 0.0f, 1.0f)); // 使用红色便于识别glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);// Draw pointsif (!m_points.empty()) {m_pointProgram.bind();m_pointProgram.setUniformValue("mvp_matrix", m_projection * m_view);m_pointProgram.setUniformValue("pointSize", m_pointSize);m_pointProgram.setUniformValue("color", m_pointColor);glPointSize(m_pointSize);m_pointBuffer.bind();m_pointProgram.enableAttributeArray(0);m_pointProgram.setAttributeBuffer(0, GL_FLOAT, 0, 3, sizeof(QVector3D));glDrawArrays(GL_POINTS, 0, m_points.size());m_pointBuffer.release();m_pointProgram.release();}drawGrids();
}void PointCloudViewer::mousePressEvent(QMouseEvent* event)
{auto pos = event->pos();if (event->button() == Qt::LeftButton) {mouse_manager.recordOneClick(pos.x(), pos.y());// 通过设置双击回调函数,这里可以打包成一个接口if (mouse_manager.isDoubleClick()){}mouse_manager.finishClick();if (!isTranslating())translating = true;}
}void PointCloudViewer::mouseReleaseEvent(QMouseEvent* event)
{mouse_manager.releaseLeftBtn();translating = false;
}bool PointCloudViewer::isTranslating() const
{return translating;
}void PointCloudViewer::mouseMoveEvent(QMouseEvent* event)
{auto pos = event->pos();glm::vec2 offsetPos = mouse_manager.updateMousePos(pos.x(), pos.y());glm::dvec2 lastMousePos = mouse_manager.lastPos();if (isTranslating()){left_plane -= offsetPos.x / (float)view_whole.w * x_span;top_plane -= offsetPos.y / (float)view_whole.h * y_span;}updateProjection();updateView();update();
}void PointCloudViewer::wheelEvent(QWheelEvent* event)
{auto pos = event->pos();mouse_manager.recordOneClick(pos.x(), pos.y());glm::vec2 lastMousePos = mouse_manager.lastPos();if (event->angleDelta().y() < 0){left_plane -= lastMousePos.x / (float)view_whole.w * x_span * 0.25;top_plane += lastMousePos.y / (float)view_whole.h * y_span * 0.25;x_span *= 1.25f;y_span *= 1.25f;}else if (event->angleDelta().y() > 0){left_plane += lastMousePos.x / (float)view_whole.w * x_span * 0.2;top_plane -= lastMousePos.y / (float)view_whole.h * y_span * 0.2;x_span *= 0.8f;y_span *= 0.8f;}updateProjection();updateView();update();
}void PointCloudViewer::initShaders()
{// Point shaderif (!m_pointProgram.addCacheableShaderFromSourceCode(QOpenGLShader::Vertex, vertexShader_point))qWarning() << "Could not compile vertex shader:" << m_pointProgram.log();if (!m_pointProgram.addCacheableShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShader_common))qWarning() << "Could not compile fragment shader:" << m_pointProgram.log();if (!m_pointProgram.link())qWarning() << "Could not link shader program:" << m_pointProgram.log();// Grid shaderif (!m_gridProgram.addCacheableShaderFromSourceCode(QOpenGLShader::Vertex, vertexShader_grid))qWarning() << "Could not compile vertex shader:" << m_gridProgram.log();if (!m_gridProgram.addCacheableShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShader_common))qWarning() << "Could not compile fragment shader:" << m_gridProgram.log();if (!m_gridProgram.link())qWarning() << "Could not link shader program:" << m_gridProgram.log();
}void PointCloudViewer::updateProjection()
{m_projection.setToIdentity();m_projection.ortho(left_plane,left_plane + x_span,top_plane - y_span,top_plane,near_plane,far_plane);
}void PointCloudViewer::updateView()
{m_view.setToIdentity();// 调整视图矩阵,确保正确观察XOZ平面m_view.lookAt(QVector3D(0, -1, 0), // 摄像机位置 (在Y轴上方)QVector3D(0, 0, 0), // 观察点 (原点)QVector3D(0, 0, 1)); // 上向量 (Z轴方向)//m_view.translate(m_translation);//m_view.scale(m_scale);
}void PointCloudViewer::calculateBoundingBox()
{if (m_points.empty()) {m_minBound = QVector3D(-1, -1, -1);m_maxBound = QVector3D(1, 1, 1);return;}m_minBound = m_points[0];m_maxBound = m_points[0];for (const auto& point : m_points) {m_minBound.setX(qMin(m_minBound.x(), point.x()));m_minBound.setY(qMin(m_minBound.y(), point.y()));m_minBound.setZ(qMin(m_minBound.z(), point.z()));m_maxBound.setX(qMax(m_maxBound.x(), point.x()));m_maxBound.setY(qMax(m_maxBound.y(), point.y()));m_maxBound.setZ(qMax(m_maxBound.z(), point.z()));}// Ensure non-zero boundsif (qFuzzyCompare(m_maxBound.x(), m_minBound.x())) {m_maxBound.setX(m_minBound.x() + 1.0f);}if (qFuzzyCompare(m_maxBound.z(), m_minBound.z())) {m_maxBound.setZ(m_minBound.z() + 1.0f);}x_span = m_maxBound.x() - m_minBound.x();y_span = x_span / window_ratio;z_span = far_plane - near_plane;window_ratio = (float)width() / (float)height();left_plane = - x_span / 2.0;top_plane = - y_span / 2.0;//right_plane = 1000.0f * window_ratio;updateProjection();updateView();update();
}QVector2D PointCloudViewer::worldToScreen(const QVector3D& worldPos) const
{QVector4D clipPos = m_projection * m_view * QVector4D(worldPos, 1.0f);QVector3D ndcPos = clipPos.toVector3D() / clipPos.w();float x = (ndcPos.x() + 1.0f) * 0.5f * width();float y = (1.0f - ndcPos.y()) * 0.5f * height(); // Note: using Z for Y in 2D projectionreturn QVector2D(x, y);
}QVector3D PointCloudViewer::screenToWorld(const QVector2D& screenPos, float y) const
{float x = (2.0f * screenPos.x() / width()) - 1.0f;float z = 1.0f - (2.0f * screenPos.y() / height());QVector4D clipPos(x, z, 0.0f, 1.0f);QVector4D worldPos = (m_projection * m_view).inverted() * clipPos;return QVector3D(worldPos.x() / worldPos.w(), y, worldPos.z() / worldPos.w());
}
MouseManager.h
#pragma once
#include<glm/glm.hpp>
#include<opencv2/opencv.hpp> // 注意ViewPort的坐标系原点是左下角点
struct ViewPort
{int w;int h;int x0;int y0;ViewPort() { w = h = x0 = y0 = 0; }ViewPort(const int& x0_, const int& y0_, const int& w_, const int& h_){w = w_;h = h_;x0 = x0_;y0 = y0_;}void init(const int& x0_, const int& y0_, const int& w_, const int& h_){w = w_;h = h_;x0 = x0_;y0 = y0_;}glm::ivec2 size(){return glm::ivec2(w, h);}glm::ivec2 bl(){return glm::ivec2(x0, y0);}glm::ivec2 tl(){return glm::ivec2(x0, y0 + h);}glm::ivec2 tr(){return glm::ivec2(x0 + w, y0 + h);}glm::ivec2 br(){return glm::ivec2(x0 + w, y0);}int midX(){return x0 + w / 2;}int midY(){return y0 + h / 2;}bool contain(int x, int y){if (x < x0 || x >= x0 + w || y < y0 || y >= y0 + h)return false;return true;}glm::vec2 NDC(const glm::vec2& inputInCurView){glm::vec2 result;result.x = (float)inputInCurView.x / w * 2.0f - 1.0f;result.y = (float)inputInCurView.y / h * 2.0f - 1.0f;return result;}// 输入x,y为当前窗口相对坐标,不是全局窗口的坐标// 归一化到[-1,1]glm::vec2 NDC(float xInCurView, float yInCurView){glm::vec2 result;result.x = xInCurView / w * 2.0f - 1.0f;result.y = yInCurView / h * 2.0f - 1.0f;return result;}glm::vec2 relativeCoord(int x, int y){return glm::vec2(x - x0, y - y0);}// 在整个窗口中的坐标// p是视口的相对坐标glm::vec2 coordInWholeView(glm::vec2 p){return glm::vec2(p.x + x0, p.y + y0);}// 给出归一化坐标X 返回绝对值坐标Xfloat absoluteCoordX(float xc){return ((xc + 1.0f) / 2.0f * w + x0);}glm::ivec2 absoluteCoord(glm::vec2 normPos){return glm::ivec2(x0 + normPos.x * w,y0 + normPos.y * h);}// 输入-1,1 返回视口内坐标 以视口的左下角点为原点glm::vec2 viewCoord(glm::vec2 p){return glm::vec2((p.x + 1.0) / 2 * w, (p.y + 1.0) / 2 * h);}float absoluteCoordY(float yc){return ((yc + 1.0f) / 2.0f * h + y0);}ViewPort& operator=(const ViewPort& v){if (this == &v) return *this;this->x0 = v.x0;this->y0 = v.y0;this->w = v.w;this->h = v.h;return *this;}float ratio(){return (static_cast<float>(w)) / (static_cast<float>(h));}void use(){glViewport(x0, y0, w, h);}
};class MouseManager
{
public:// 这里的x,y是viewport内坐标系的坐标值,而不是渲染窗口的坐标值struct MouseButton {bool IsPressed = false;int x;int y;};void setViewPort(ViewPort* viewport){_viewport = viewport;}bool isInArea(int x, int y){if (_viewport == nullptr)return false;return _viewport->contain(x, _scr_height - y);}bool isInArea(){if (_viewport == nullptr)return false;return _viewport->contain(lastX + _viewport->x0, _viewport->h - lastY + _viewport->y0);}void releaseLeftBtn(){leftBtn.IsPressed = false;}void recordOneClick(int x, int y){x -= (_viewport->tl().x);y -= (_scr_height - _viewport->tl().y);clickStamp1 = cv::getTickCount();leftBtn.x = x;leftBtn.y = y;lastX = x;lastY = y;}bool isDoubleClick(double thresh = 200.0){// 判断是否双击if (clickStamp2 != -1){double duration = (clickStamp1 - clickStamp2) / cv::getTickFrequency() * 1000.0;printf("duration = %lf\n", duration);if (duration < thresh){return true;}}return false;}void finishClick(){if (clickStamp1 != -1){clickStamp2 = clickStamp1;}leftBtn.IsPressed = true;}// 返回左键单击点的纹理坐标,ViewPort的左下角点为坐标原点glm::ivec2 getLeftBtnTextureCoord(){return glm::ivec2(leftBtn.x,_viewport->h - leftBtn.y - 1);}glm::vec2 updateMousePos(double x, double y){x -= (_viewport->tl().x);y -= (_scr_height - _viewport->tl().y);if (firstMouse) // this bool variable is initially set to true{lastX = x;lastY = y;firstMouse = false;}float xoffset = x - lastX;float yoffset = lastY - y; // reversed since y-coordinates range from bottom to toplastX = x;lastY = y;leftBtn.x = x;leftBtn.y = y;//m_leftMouseButton.x = xpos;//m_leftMouseButton.y = ypos;//patternCam.setMousePos(glm::vec2(lastX, lastY));return glm::vec2(xoffset, yoffset);}// lastX lastY以_viewport的左上角点为坐标系原点glm::dvec2 lastPos(){return glm::dvec2(lastX, lastY);}void setScreenSize(int scr_width, int scr_height){_scr_width = scr_width;_scr_height = scr_height;}MouseButton leftBtn, rightBtn;
private:bool firstMouse = true;ViewPort* _viewport = nullptr;int _scr_width, _scr_height;// lastX lastY以_viewport的左上角点为坐标系原点double lastX = 0;double lastY = 0;// 用于判断是否是双击 int clickStamp1 = -1, clickStamp2 = -1;
};