--- a/src/ui/canvas.cpp Tue Jun 28 19:25:45 2022 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,431 +0,0 @@ -#include <QMouseEvent> -#include <QPainter> -#include "document.h" -#include "canvas.h" - -Canvas::Canvas( - Model* model, - EditTools *document, - DocumentManager* documents, - const ColorTable& colorTable, - QWidget* parent) : - PartRenderer{model, documents, colorTable, parent}, - document{document} -{ - this->setMouseTracking(true); -} - -/** - * @brief Handles a change of selection - * @param selectedIds IDs of objects to select - * @param deselectedIds IDs of objects to deselect. - */ -void Canvas::handleSelectionChange(const QSet<ModelId> &selectedIds, const QSet<ModelId> &deselectedIds) -{ - Q_ASSERT(not selectedIds.contains({0})); - this->selection.subtract(deselectedIds); - this->selection.unite(selectedIds); - gl::setModelShaderSelectedObjects(&this->shaders, this->selection); - this->update(); -} - -/** - * @brief Updates vertex rendering - * @param document Document to get vertices from - */ -void Canvas::rebuildVertices(VertexMap* vertexMap) -{ - if (this->vertexProgram.has_value()) - { - this->vertexProgram->build(vertexMap); - this->update(); - } -} - -void Canvas::mouseMoveEvent(QMouseEvent* event) -{ - const ModelId id = this->pick(event->pos()); - this->highlighted = id; - this->totalMouseMove += (event->pos() - this->lastMousePosition).manhattanLength(); - this->worldPosition = this->screenToModelCoordinates(event->pos(), this->gridPlane); - if (this->worldPosition.has_value()) - { - /* - * Snap the position to grid. This procedure is basically the "change of basis" and almost follows the - * Aโปยน ร M ร A formula which is used to perform a transformation in some other coordinate system, except - * we actually use the inverted matrix first and the regular one last to perform the transformation of - * grid coordinates in our XY coordinate system. Also, we're rounding the coordinates which is obviously - * not a linear transformation, but fits the pattern anyway. - */ - // First transform the coordinates to the XY plane... - this->worldPosition = glm::inverse(this->gridMatrix) * glm::vec4{*this->worldPosition, 1}; - // Then round the coordinates to integer precision... - this->worldPosition = glm::round(*this->worldPosition); - // And finally transform it back to grid coordinates by transforming it with the - // grid matrix. - this->worldPosition = this->gridMatrix * glm::vec4{*this->worldPosition, 1}; - } - Q_EMIT this->mouseMove(event); - PartRenderer::mouseMoveEvent(event); - this->update(); -} - -void Canvas::mousePressEvent(QMouseEvent* event) -{ - this->totalMouseMove = 0; - this->lastMousePosition = event->pos(); - PartRenderer::mousePressEvent(event); -} - -void Canvas::mouseReleaseEvent(QMouseEvent* event) -{ - if (this->totalMouseMove < (2.0 / sqrt(2)) * 5.0) - { - Q_EMIT this->mouseClick(event); - } - PartRenderer::mouseReleaseEvent(event); - this->update(); -} - -void Canvas::initializeGL() -{ - // We first create the grid program and connect everything and only then call the part renderer's initialization - // functions so that when initialization sets up, the signals also set up the matrices on our side. - this->gridProgram.emplace(this); - this->gridProgram->initialize(); - this->axesProgram.emplace(this); - this->axesProgram->initialize(); - this->vertexProgram.emplace(this); - this->vertexProgram->initialize(); - for (AbstractBasicShaderProgram* program : { - static_cast<AbstractBasicShaderProgram*>(&*this->gridProgram), - static_cast<AbstractBasicShaderProgram*>(&*this->axesProgram), - static_cast<AbstractBasicShaderProgram*>(&*this->vertexProgram), - }) - { - connect(this, &PartRenderer::projectionMatrixChanged, - program, &AbstractBasicShaderProgram::setProjectionMatrix); - connect(this, &PartRenderer::modelMatrixChanged, - program, &AbstractBasicShaderProgram::setModelMatrix); - connect(this, &PartRenderer::viewMatrixChanged, - program, &AbstractBasicShaderProgram::setViewMatrix); - } - connect(this, &PartRenderer::renderPreferencesChanged, this, &Canvas::updateCanvasRenderPreferences); - PartRenderer::initializeGL(); - // Set up XZ grid matrix - this->setGridMatrix({{1, 0, 0, 0}, {0, 0, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 1}}); - this->updateCanvasRenderPreferences(); -} - -static const struct -{ - const QBrush pointBrush = {Qt::white}; - const QPen polygonPen = {QBrush{Qt::black}, 2.0, Qt::DashLine}; - const QPen badPolygonPen = {QBrush{Qt::red}, 2.0, Qt::DashLine}; - const QPen pointPen = {QBrush{Qt::black}, 2.0}; - const QBrush greenPolygonBrush = {QColor{64, 255, 128, 192}}; - const QBrush redPolygonBrush = {QColor{255, 96, 96, 192}}; -} pens; - -static void renderDrawState( - QPainter* painter, - Canvas* canvas, - DrawState* drawState); - -void Canvas::paintGL() -{ - PartRenderer::paintGL(); - if (this->renderPreferences.style != gl::RenderStyle::PickScene) - { - // Render axes - if (this->renderPreferences.drawAxes) - { - glLineWidth(5); - glEnable(GL_LINE_SMOOTH); - glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); - this->axesProgram->draw(); - glDisable(GL_LINE_SMOOTH); - } - // Render vertices - { - glCullFace(GL_FRONT); - this->vertexProgram->draw(); - } - // Render grid - { - glLineWidth(1); - glEnable(GL_BLEND); - glLineStipple(1, 0x8888); - glEnable(GL_LINE_STIPPLE); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - this->gridProgram->draw(); - glDisable(GL_BLEND); - glDisable(GL_LINE_STIPPLE); - } - if (this->worldPosition.has_value()) - { - QPainter painter{this}; - painter.setRenderHint(QPainter::Antialiasing); - painter.setPen(this->isDark ? Qt::white : Qt::black); - painter.setBrush(Qt::green); - const QPointF pos = this->modelToScreenCoordinates(*this->worldPosition); - painter.drawEllipse(pos, 5, 5); - painter.drawText(pos + QPointF{5, 5}, vectorToString(*this->worldPosition)); - } - QPainter painter{this}; - painter.setRenderHint(QPainter::Antialiasing); - if (this->renderPreferences.drawAxes) - { - this->renderAxesLabels(painter); - } - if (this->drawState != nullptr) { - renderDrawState(&painter, this, this->drawState); - } - } -} - -static void renderDrawState( - QPainter* painter, - Canvas* canvas, - DrawState* drawState) -{ - switch(drawState->mode) - { - case SelectMode: - break; - case DrawMode: - { - painter->setPen(drawState->isconcave ? ::pens.badPolygonPen : ::pens.polygonPen); - if (drawState->previewPolygon.size() > 2 and not drawState->isconcave) - { - if (canvas->worldPolygonWinding(drawState->previewPolygon) == Winding::Clockwise) - { - painter->setBrush(::pens.greenPolygonBrush); - } - else - { - painter->setBrush(::pens.redPolygonBrush); - } - canvas->drawWorldPolygon(painter, drawState->previewPolygon); - } - else - { - canvas->drawWorldPolyline(painter, drawState->previewPolygon); - } - painter->setBrush(::pens.pointBrush); - painter->setPen(::pens.pointPen); - for (const glm::vec3& point : drawState->polygon) - { - canvas->drawWorldPoint(painter, point); - } - canvas->drawWorldPoint(painter, drawState->previewPoint); - } - break; - } -} - -/** - * @brief Renders labels such as +x at the ends of axes at the screen - * @param painter - */ -void Canvas::renderAxesLabels(QPainter& painter) -{ - QFont font; - //font.setStyle(QFont::StyleItalic); - painter.setFont(font); - QFontMetrics fontMetrics{font}; - const auto renderText = [&](const QString& text, const PointOnRectagle& intersection) - { - QPointF position = toQPointF(intersection.position); - const RectangleSide side = intersection.side; - switch (side) - { - case RectangleSide::Top: - position += QPointF{0, static_cast<qreal>(fontMetrics.ascent())}; - break; - case RectangleSide::Left: - break; - case RectangleSide::Bottom: - position += QPointF{0, static_cast<qreal>(-fontMetrics.descent())}; - break; - case RectangleSide::Right: - position += QPointF{static_cast<qreal>(-fontMetrics.horizontalAdvance(text)), 0}; - break; - } - painter.drawText(position, text); - }; - const QRectF box {QPointF{0, 0}, sizeToSizeF(this->size())}; - const QPointF p1 = this->modelToScreenCoordinates(glm::vec3{0, 0, 0}); - static const struct - { - QString text; - glm::vec3 direction; - } directions[] = - { - {"+๐ฅ", {1, 0, 0}}, - {"-๐ฅ", {-1, 0, 0}}, - {"+๐ฆ", {0, 1, 0}}, - {"-๐ฆ", {0, -1, 0}}, - {"+๐ง", {0, 0, 1}}, - {"-๐ง", {0, 0, -1}}, - }; - for (const auto& axis : directions) - { - const QPointF x_p = this->modelToScreenCoordinates(axis.direction); - const auto intersection = rayRectangleIntersection( - rayFromPoints(toVec2(p1), toVec2(x_p)), - box); - if (intersection.has_value()) - { - renderText(axis.text, *intersection); - } - } -} - -/** - * @brief Draws a polyline to where the specified vector of 3D points would appear on the screen. - * @param painter Painter to use to draw with - * @param points 3D points to render - */ -void Canvas::drawWorldPolyline(QPainter *painter, const std::vector<glm::vec3> &points) -{ - painter->drawPolyline(QPolygonF{this->convertWorldPointsToScreenPoints(points)}); -} - -/** - * @brief Draws a polygon to where the specified vector of 3D points would appear on the screen. - * @param painter Painter to use to draw with - * @param points 3D points to render - */ -void Canvas::drawWorldPolygon(QPainter* painter, const std::vector<glm::vec3> &points) -{ - painter->drawPolygon(QPolygonF{this->convertWorldPointsToScreenPoints(points)}); -} - -Winding Canvas::worldPolygonWinding(const std::vector<glm::vec3> &points) const -{ - return winding(QPolygonF{this->convertWorldPointsToScreenPoints(points)}); -} - -/** - * @brief Gets the current position of the cursor in the model - * @return 3D vector - */ -const std::optional<glm::vec3>& Canvas::getWorldPosition() const -{ - return this->worldPosition; -} - -/** - * @brief Adjusts the grid to be so that it is perpendicular to the camera. - */ -void adjustGridToView(Canvas* canvas) -{ - const glm::vec3 cameraDirection = canvas->cameraVector(); - const glm::mat4& grid = canvas->getGridMatrix(); - const glm::vec3 vector_x = glm::normalize(grid * glm::vec4{1, 0, 0, 1}); - const glm::vec3 vector_y = glm::normalize(grid * glm::vec4{0, 1, 0, 1}); - const float angle_x = std::abs(glm::dot(vector_x, cameraDirection)); - const float angle_y = std::abs(glm::dot(vector_y, cameraDirection)); - canvas->setGridMatrix(glm::rotate( - grid, - pi<> * 0.5f, - (angle_x < angle_y) ? glm::vec3{1, 0, 0} : glm::vec3{0, 1, 0} - )); - canvas->update(); -} - -/** - * @returns the ids of the currently selected objects - */ -const QSet<ModelId> Canvas::selectedObjects() const -{ - return this->selection; -} - -const glm::mat4 &Canvas::getGridMatrix() const -{ - return this->gridMatrix; -} - -/** - * @brief Paints a circle at where @c worldPoint is located on the screen. - * @param painter Painter to use to render - * @param worldPoint Point to render - */ -void Canvas::drawWorldPoint(QPainter* painter, const glm::vec3& worldPoint) const -{ - const QPointF center = this->modelToScreenCoordinates(worldPoint); - painter->drawEllipse(inscribe(CircleF{center, 5})); -} - -/** - * @brief Changes the grid matrix to the one specified. Updates relevant member variables. - * @param newMatrix New matrix to use - */ -void Canvas::setGridMatrix(const glm::mat4& newMatrix) -{ - this->gridMatrix = newMatrix; - const Triangle triangle { - this->gridMatrix * glm::vec4{0, 0, 0, 1}, - this->gridMatrix * glm::vec4{1, 0, 0, 1}, - this->gridMatrix * glm::vec4{0, 1, 0, 1}, - }; - this->gridPlane = planeFromTriangle(triangle); - this->gridProgram->setGridMatrix(this->gridMatrix); - this->update(); -} - -/** - * @brief Gets the current camera vector, i.e. the vector from the camera to the grid origin. - * @return vector - */ -glm::vec3 Canvas::cameraVector() const -{ - // Find out where the grid is projected on the screen - const QPointF gridOrigin2d = this->modelToScreenCoordinates(this->gridPlane.anchor); - // Find out which direction the camera is looking at the grid origin in 3d - return glm::normalize(this->cameraLine(gridOrigin2d).direction); -} - -/** - * @brief Calculates whether the screen is perpendicular to the current grid - * @return bool - */ -bool Canvas::isGridPerpendicularToScreen(float threshold) const -{ - const glm::vec3 cameraDirection = this->cameraVector(); - // Compute the dot product. The parameters given are: - // - the normal of the grid plane, which is the vector from the grid origin perpendicular to the grid - // - the direction of the camera looking at the grid, which is the inverse of the vector from the grid - // origin towards the camera - // If the dot product between these two vectors is 0, the grid normal is perpendicular to the camera vector - // and the grid is perpendicular to the screen. - const float dot = glm::dot(glm::normalize(this->gridPlane.normal), glm::normalize(cameraDirection)); - return std::abs(dot) < threshold; -} - -QVector<QPointF> Canvas::convertWorldPointsToScreenPoints(const std::vector<glm::vec3> &worldPoints) const -{ - QVector<QPointF> points2d; - points2d.reserve(worldPoints.size()); - for (const glm::vec3& point : worldPoints) - { - points2d.push_back(this->modelToScreenCoordinates(point)); - } - return points2d; -} - -void Canvas::updateCanvasRenderPreferences() -{ - this->isDark = luma(this->renderPreferences.backgroundColor) < 0.25; - if (this->gridProgram.has_value()) - { - this->gridProgram->setGridColor(this->isDark ? Qt::white : Qt::black); - } -} - -void Canvas::setOverpaintCallback(Canvas::OverpaintCallback fn) -{ - this->overpaintCallback = fn; -}