Wed, 25 May 2022 17:42:02 +0300
Simplify PolygonCache
#include <QMouseEvent> #include <QPainter> #include "modeleditor.h" #include "document.h" #include "canvas.h" #include "linetypes/edge.h" #include "linetypes/triangle.h" #include "linetypes/quadrilateral.h" Canvas::Canvas( Model* model, Document *document, DocumentManager* documents, const ldraw::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<ldraw::id_t>& selectedIds, const QSet<ldraw::id_t>& deselectedIds) { Q_ASSERT(not selectedIds.contains(ldraw::NULL_ID)); 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(Document* document) { if (this->vertexProgram.has_value()) { this->vertexProgram->build(document); this->update(); } } void updatePreviewPolygon(DrawState* drawState) { drawState->previewPolygon = drawState->polygon; drawState->previewPolygon.resize(drawState->polygon.size() + 1); drawState->previewPolygon.back() = drawState->previewPoint; if (drawState->previewPolygon.size() > 2) { drawState->isconcave = not geom::isConvex(drawState->previewPolygon); } } void removeLastPoint(DrawState* drawState) { if (drawState->polygon.size() > 0) { drawState->polygon.erase(drawState->polygon.end() - 1); updatePreviewPolygon(drawState); } } bool isCloseToExistingPoints(const std::vector<glm::vec3>& points, const glm::vec3 &pos) { return any(points, std::bind(geom::isclose, std::placeholders::_1, pos)); } void Canvas::mouseMoveEvent(QMouseEvent* event) { const ldraw::id_t 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}; } switch(this->mode) { case SelectMode: break; case DrawMode: const auto& worldPosition = this->getWorldPosition(); if (worldPosition.has_value()) { this->drawState.previewPoint = worldPosition.value(); updatePreviewPolygon(&this->drawState); this->update(); } event->accept(); break; } 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) { switch(this->mode) { case SelectMode: if (event->button() == Qt::LeftButton) { const ldraw::id_t highlighted = this->getHighlightedObject(); this->clearSelection(); if (highlighted != ldraw::NULL_ID) { this->addToSelection(highlighted); } event->accept(); } break; case DrawMode: if (event->button() == Qt::LeftButton and this->worldPosition.has_value()) { const glm::vec3& pos = worldPosition.value(); if (isCloseToExistingPoints(this->drawState.polygon, pos)) { this->closeShape(); } else { this->drawState.polygon.push_back(pos); updatePreviewPolygon(&this->drawState); } event->accept(); } else if (true and event->button() == Qt::RightButton and this->drawState.polygon.size() > 0 ) { this->drawState.polygon.erase(this->drawState.polygon.end() - 1); updatePreviewPolygon(&this->drawState); event->accept(); } break; } } 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; 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); } switch(this->mode) { case SelectMode: break; case DrawMode: { painter.setPen(this->drawState.isconcave ? ::pens.badPolygonPen : ::pens.polygonPen); if (this->drawState.previewPolygon.size() > 2 and not this->drawState.isconcave) { if (this->worldPolygonWinding(this->drawState.previewPolygon) == Winding::Clockwise) { painter.setBrush(::pens.greenPolygonBrush); } else { painter.setBrush(::pens.redPolygonBrush); } this->drawWorldPolygon(&painter, this->drawState.previewPolygon); } else { this->drawWorldPolyline(&painter, this->drawState.previewPolygon); } painter.setBrush(::pens.pointBrush); painter.setPen(::pens.pointPen); for (const glm::vec3& point : this->drawState.polygon) { this->drawWorldPoint(&painter, point); } this->drawWorldPoint(&painter, this->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 geom::PointOnRectagle& intersection) { QPointF position = toQPointF(intersection.position); const geom::RectangleSide side = intersection.side; switch (side) { case geom::RectangleSide::Top: position += QPointF{0, static_cast<qreal>(fontMetrics.ascent())}; break; case geom::RectangleSide::Left: break; case geom::RectangleSide::Bottom: position += QPointF{0, static_cast<qreal>(-fontMetrics.descent())}; break; case geom::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 = geom::rayRectangleIntersection( geom::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 geom::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<ldraw::id_t> Canvas::selectedObjects() const { return this->selection; } const glm::mat4 &Canvas::getGridMatrix() const { return this->gridMatrix; } void Canvas::closeShape() { if (this->drawState.polygon.size() >= 2 and this->drawState.polygon.size() <= 4) { std::unique_ptr<ModelEditor> modelEditor = this->document->editModel(); switch (this->drawState.polygon.size()) { case 2: modelEditor->append<ldraw::Edge>( vectorToArray<2>(this->drawState.polygon), ldraw::EDGE_COLOR); break; case 3: modelEditor->append<ldraw::Triangle>( vectorToArray<3>(this->drawState.polygon), ldraw::MAIN_COLOR); break; case 4: modelEditor->append<ldraw::Quadrilateral>( vectorToArray<4>(this->drawState.polygon), ldraw::MAIN_COLOR); break; } } this->drawState.polygon.clear(); updatePreviewPolygon(&this->drawState); } /** * @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(geom::inscribe(geom::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 geom::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 = geom::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 QPoint gridOrigin2d = pointFToPoint(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); } } /** * @brief Clears the selection. */ void Canvas::clearSelection() { this->selection.clear(); gl::setModelShaderSelectedObjects(&this->shaders, this->selection); Q_EMIT selectionChanged(this->selection); this->update(); } /** * @brief Adds an object to selection. * @param id ID of object to add */ void Canvas::addToSelection(ldraw::id_t id) { this->selection.insert(id); gl::setModelShaderSelectedObjects(&this->shaders, this->selection); Q_EMIT selectionChanged(this->selection); this->update(); } void Canvas::setOverpaintCallback(Canvas::OverpaintCallback fn) { this->overpaintCallback = fn; }