src/ui/canvas.cpp

changeset 296
38f6fad61bad
parent 295
4241d948af28
child 297
bc92f97498f7
equal deleted inserted replaced
295:4241d948af28 296:38f6fad61bad
1 #include <QMouseEvent>
2 #include <QPainter>
3 #include "document.h"
4 #include "canvas.h"
5
6 Canvas::Canvas(
7 Model* model,
8 EditTools *document,
9 DocumentManager* documents,
10 const ColorTable& colorTable,
11 QWidget* parent) :
12 PartRenderer{model, documents, colorTable, parent},
13 document{document}
14 {
15 this->setMouseTracking(true);
16 }
17
18 /**
19 * @brief Handles a change of selection
20 * @param selectedIds IDs of objects to select
21 * @param deselectedIds IDs of objects to deselect.
22 */
23 void Canvas::handleSelectionChange(const QSet<ModelId> &selectedIds, const QSet<ModelId> &deselectedIds)
24 {
25 Q_ASSERT(not selectedIds.contains({0}));
26 this->selection.subtract(deselectedIds);
27 this->selection.unite(selectedIds);
28 gl::setModelShaderSelectedObjects(&this->shaders, this->selection);
29 this->update();
30 }
31
32 /**
33 * @brief Updates vertex rendering
34 * @param document Document to get vertices from
35 */
36 void Canvas::rebuildVertices(VertexMap* vertexMap)
37 {
38 if (this->vertexProgram.has_value())
39 {
40 this->vertexProgram->build(vertexMap);
41 this->update();
42 }
43 }
44
45 void Canvas::mouseMoveEvent(QMouseEvent* event)
46 {
47 const ModelId id = this->pick(event->pos());
48 this->highlighted = id;
49 this->totalMouseMove += (event->pos() - this->lastMousePosition).manhattanLength();
50 this->worldPosition = this->screenToModelCoordinates(event->pos(), this->gridPlane);
51 if (this->worldPosition.has_value())
52 {
53 /*
54 * Snap the position to grid. This procedure is basically the "change of basis" and almost follows the
55 * Aโปยน ร— M ร— A formula which is used to perform a transformation in some other coordinate system, except
56 * we actually use the inverted matrix first and the regular one last to perform the transformation of
57 * grid coordinates in our XY coordinate system. Also, we're rounding the coordinates which is obviously
58 * not a linear transformation, but fits the pattern anyway.
59 */
60 // First transform the coordinates to the XY plane...
61 this->worldPosition = glm::inverse(this->gridMatrix) * glm::vec4{*this->worldPosition, 1};
62 // Then round the coordinates to integer precision...
63 this->worldPosition = glm::round(*this->worldPosition);
64 // And finally transform it back to grid coordinates by transforming it with the
65 // grid matrix.
66 this->worldPosition = this->gridMatrix * glm::vec4{*this->worldPosition, 1};
67 }
68 Q_EMIT this->mouseMove(event);
69 PartRenderer::mouseMoveEvent(event);
70 this->update();
71 }
72
73 void Canvas::mousePressEvent(QMouseEvent* event)
74 {
75 this->totalMouseMove = 0;
76 this->lastMousePosition = event->pos();
77 PartRenderer::mousePressEvent(event);
78 }
79
80 void Canvas::mouseReleaseEvent(QMouseEvent* event)
81 {
82 if (this->totalMouseMove < (2.0 / sqrt(2)) * 5.0)
83 {
84 Q_EMIT this->mouseClick(event);
85 }
86 PartRenderer::mouseReleaseEvent(event);
87 this->update();
88 }
89
90 void Canvas::initializeGL()
91 {
92 // We first create the grid program and connect everything and only then call the part renderer's initialization
93 // functions so that when initialization sets up, the signals also set up the matrices on our side.
94 this->gridProgram.emplace(this);
95 this->gridProgram->initialize();
96 this->axesProgram.emplace(this);
97 this->axesProgram->initialize();
98 this->vertexProgram.emplace(this);
99 this->vertexProgram->initialize();
100 for (AbstractBasicShaderProgram* program : {
101 static_cast<AbstractBasicShaderProgram*>(&*this->gridProgram),
102 static_cast<AbstractBasicShaderProgram*>(&*this->axesProgram),
103 static_cast<AbstractBasicShaderProgram*>(&*this->vertexProgram),
104 })
105 {
106 connect(this, &PartRenderer::projectionMatrixChanged,
107 program, &AbstractBasicShaderProgram::setProjectionMatrix);
108 connect(this, &PartRenderer::modelMatrixChanged,
109 program, &AbstractBasicShaderProgram::setModelMatrix);
110 connect(this, &PartRenderer::viewMatrixChanged,
111 program, &AbstractBasicShaderProgram::setViewMatrix);
112 }
113 connect(this, &PartRenderer::renderPreferencesChanged, this, &Canvas::updateCanvasRenderPreferences);
114 PartRenderer::initializeGL();
115 // Set up XZ grid matrix
116 this->setGridMatrix({{1, 0, 0, 0}, {0, 0, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 1}});
117 this->updateCanvasRenderPreferences();
118 }
119
120 static const struct
121 {
122 const QBrush pointBrush = {Qt::white};
123 const QPen polygonPen = {QBrush{Qt::black}, 2.0, Qt::DashLine};
124 const QPen badPolygonPen = {QBrush{Qt::red}, 2.0, Qt::DashLine};
125 const QPen pointPen = {QBrush{Qt::black}, 2.0};
126 const QBrush greenPolygonBrush = {QColor{64, 255, 128, 192}};
127 const QBrush redPolygonBrush = {QColor{255, 96, 96, 192}};
128 } pens;
129
130 static void renderDrawState(
131 QPainter* painter,
132 Canvas* canvas,
133 DrawState* drawState);
134
135 void Canvas::paintGL()
136 {
137 PartRenderer::paintGL();
138 if (this->renderPreferences.style != gl::RenderStyle::PickScene)
139 {
140 // Render axes
141 if (this->renderPreferences.drawAxes)
142 {
143 glLineWidth(5);
144 glEnable(GL_LINE_SMOOTH);
145 glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
146 this->axesProgram->draw();
147 glDisable(GL_LINE_SMOOTH);
148 }
149 // Render vertices
150 {
151 glCullFace(GL_FRONT);
152 this->vertexProgram->draw();
153 }
154 // Render grid
155 {
156 glLineWidth(1);
157 glEnable(GL_BLEND);
158 glLineStipple(1, 0x8888);
159 glEnable(GL_LINE_STIPPLE);
160 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
161 this->gridProgram->draw();
162 glDisable(GL_BLEND);
163 glDisable(GL_LINE_STIPPLE);
164 }
165 if (this->worldPosition.has_value())
166 {
167 QPainter painter{this};
168 painter.setRenderHint(QPainter::Antialiasing);
169 painter.setPen(this->isDark ? Qt::white : Qt::black);
170 painter.setBrush(Qt::green);
171 const QPointF pos = this->modelToScreenCoordinates(*this->worldPosition);
172 painter.drawEllipse(pos, 5, 5);
173 painter.drawText(pos + QPointF{5, 5}, vectorToString(*this->worldPosition));
174 }
175 QPainter painter{this};
176 painter.setRenderHint(QPainter::Antialiasing);
177 if (this->renderPreferences.drawAxes)
178 {
179 this->renderAxesLabels(painter);
180 }
181 if (this->drawState != nullptr) {
182 renderDrawState(&painter, this, this->drawState);
183 }
184 }
185 }
186
187 static void renderDrawState(
188 QPainter* painter,
189 Canvas* canvas,
190 DrawState* drawState)
191 {
192 switch(drawState->mode)
193 {
194 case SelectMode:
195 break;
196 case DrawMode:
197 {
198 painter->setPen(drawState->isconcave ? ::pens.badPolygonPen : ::pens.polygonPen);
199 if (drawState->previewPolygon.size() > 2 and not drawState->isconcave)
200 {
201 if (canvas->worldPolygonWinding(drawState->previewPolygon) == Winding::Clockwise)
202 {
203 painter->setBrush(::pens.greenPolygonBrush);
204 }
205 else
206 {
207 painter->setBrush(::pens.redPolygonBrush);
208 }
209 canvas->drawWorldPolygon(painter, drawState->previewPolygon);
210 }
211 else
212 {
213 canvas->drawWorldPolyline(painter, drawState->previewPolygon);
214 }
215 painter->setBrush(::pens.pointBrush);
216 painter->setPen(::pens.pointPen);
217 for (const glm::vec3& point : drawState->polygon)
218 {
219 canvas->drawWorldPoint(painter, point);
220 }
221 canvas->drawWorldPoint(painter, drawState->previewPoint);
222 }
223 break;
224 }
225 }
226
227 /**
228 * @brief Renders labels such as +x at the ends of axes at the screen
229 * @param painter
230 */
231 void Canvas::renderAxesLabels(QPainter& painter)
232 {
233 QFont font;
234 //font.setStyle(QFont::StyleItalic);
235 painter.setFont(font);
236 QFontMetrics fontMetrics{font};
237 const auto renderText = [&](const QString& text, const PointOnRectagle& intersection)
238 {
239 QPointF position = toQPointF(intersection.position);
240 const RectangleSide side = intersection.side;
241 switch (side)
242 {
243 case RectangleSide::Top:
244 position += QPointF{0, static_cast<qreal>(fontMetrics.ascent())};
245 break;
246 case RectangleSide::Left:
247 break;
248 case RectangleSide::Bottom:
249 position += QPointF{0, static_cast<qreal>(-fontMetrics.descent())};
250 break;
251 case RectangleSide::Right:
252 position += QPointF{static_cast<qreal>(-fontMetrics.horizontalAdvance(text)), 0};
253 break;
254 }
255 painter.drawText(position, text);
256 };
257 const QRectF box {QPointF{0, 0}, sizeToSizeF(this->size())};
258 const QPointF p1 = this->modelToScreenCoordinates(glm::vec3{0, 0, 0});
259 static const struct
260 {
261 QString text;
262 glm::vec3 direction;
263 } directions[] =
264 {
265 {"+๐‘ฅ", {1, 0, 0}},
266 {"-๐‘ฅ", {-1, 0, 0}},
267 {"+๐‘ฆ", {0, 1, 0}},
268 {"-๐‘ฆ", {0, -1, 0}},
269 {"+๐‘ง", {0, 0, 1}},
270 {"-๐‘ง", {0, 0, -1}},
271 };
272 for (const auto& axis : directions)
273 {
274 const QPointF x_p = this->modelToScreenCoordinates(axis.direction);
275 const auto intersection = rayRectangleIntersection(
276 rayFromPoints(toVec2(p1), toVec2(x_p)),
277 box);
278 if (intersection.has_value())
279 {
280 renderText(axis.text, *intersection);
281 }
282 }
283 }
284
285 /**
286 * @brief Draws a polyline to where the specified vector of 3D points would appear on the screen.
287 * @param painter Painter to use to draw with
288 * @param points 3D points to render
289 */
290 void Canvas::drawWorldPolyline(QPainter *painter, const std::vector<glm::vec3> &points)
291 {
292 painter->drawPolyline(QPolygonF{this->convertWorldPointsToScreenPoints(points)});
293 }
294
295 /**
296 * @brief Draws a polygon to where the specified vector of 3D points would appear on the screen.
297 * @param painter Painter to use to draw with
298 * @param points 3D points to render
299 */
300 void Canvas::drawWorldPolygon(QPainter* painter, const std::vector<glm::vec3> &points)
301 {
302 painter->drawPolygon(QPolygonF{this->convertWorldPointsToScreenPoints(points)});
303 }
304
305 Winding Canvas::worldPolygonWinding(const std::vector<glm::vec3> &points) const
306 {
307 return winding(QPolygonF{this->convertWorldPointsToScreenPoints(points)});
308 }
309
310 /**
311 * @brief Gets the current position of the cursor in the model
312 * @return 3D vector
313 */
314 const std::optional<glm::vec3>& Canvas::getWorldPosition() const
315 {
316 return this->worldPosition;
317 }
318
319 /**
320 * @brief Adjusts the grid to be so that it is perpendicular to the camera.
321 */
322 void adjustGridToView(Canvas* canvas)
323 {
324 const glm::vec3 cameraDirection = canvas->cameraVector();
325 const glm::mat4& grid = canvas->getGridMatrix();
326 const glm::vec3 vector_x = glm::normalize(grid * glm::vec4{1, 0, 0, 1});
327 const glm::vec3 vector_y = glm::normalize(grid * glm::vec4{0, 1, 0, 1});
328 const float angle_x = std::abs(glm::dot(vector_x, cameraDirection));
329 const float angle_y = std::abs(glm::dot(vector_y, cameraDirection));
330 canvas->setGridMatrix(glm::rotate(
331 grid,
332 pi<> * 0.5f,
333 (angle_x < angle_y) ? glm::vec3{1, 0, 0} : glm::vec3{0, 1, 0}
334 ));
335 canvas->update();
336 }
337
338 /**
339 * @returns the ids of the currently selected objects
340 */
341 const QSet<ModelId> Canvas::selectedObjects() const
342 {
343 return this->selection;
344 }
345
346 const glm::mat4 &Canvas::getGridMatrix() const
347 {
348 return this->gridMatrix;
349 }
350
351 /**
352 * @brief Paints a circle at where @c worldPoint is located on the screen.
353 * @param painter Painter to use to render
354 * @param worldPoint Point to render
355 */
356 void Canvas::drawWorldPoint(QPainter* painter, const glm::vec3& worldPoint) const
357 {
358 const QPointF center = this->modelToScreenCoordinates(worldPoint);
359 painter->drawEllipse(inscribe(CircleF{center, 5}));
360 }
361
362 /**
363 * @brief Changes the grid matrix to the one specified. Updates relevant member variables.
364 * @param newMatrix New matrix to use
365 */
366 void Canvas::setGridMatrix(const glm::mat4& newMatrix)
367 {
368 this->gridMatrix = newMatrix;
369 const Triangle triangle {
370 this->gridMatrix * glm::vec4{0, 0, 0, 1},
371 this->gridMatrix * glm::vec4{1, 0, 0, 1},
372 this->gridMatrix * glm::vec4{0, 1, 0, 1},
373 };
374 this->gridPlane = planeFromTriangle(triangle);
375 this->gridProgram->setGridMatrix(this->gridMatrix);
376 this->update();
377 }
378
379 /**
380 * @brief Gets the current camera vector, i.e. the vector from the camera to the grid origin.
381 * @return vector
382 */
383 glm::vec3 Canvas::cameraVector() const
384 {
385 // Find out where the grid is projected on the screen
386 const QPointF gridOrigin2d = this->modelToScreenCoordinates(this->gridPlane.anchor);
387 // Find out which direction the camera is looking at the grid origin in 3d
388 return glm::normalize(this->cameraLine(gridOrigin2d).direction);
389 }
390
391 /**
392 * @brief Calculates whether the screen is perpendicular to the current grid
393 * @return bool
394 */
395 bool Canvas::isGridPerpendicularToScreen(float threshold) const
396 {
397 const glm::vec3 cameraDirection = this->cameraVector();
398 // Compute the dot product. The parameters given are:
399 // - the normal of the grid plane, which is the vector from the grid origin perpendicular to the grid
400 // - the direction of the camera looking at the grid, which is the inverse of the vector from the grid
401 // origin towards the camera
402 // If the dot product between these two vectors is 0, the grid normal is perpendicular to the camera vector
403 // and the grid is perpendicular to the screen.
404 const float dot = glm::dot(glm::normalize(this->gridPlane.normal), glm::normalize(cameraDirection));
405 return std::abs(dot) < threshold;
406 }
407
408 QVector<QPointF> Canvas::convertWorldPointsToScreenPoints(const std::vector<glm::vec3> &worldPoints) const
409 {
410 QVector<QPointF> points2d;
411 points2d.reserve(worldPoints.size());
412 for (const glm::vec3& point : worldPoints)
413 {
414 points2d.push_back(this->modelToScreenCoordinates(point));
415 }
416 return points2d;
417 }
418
419 void Canvas::updateCanvasRenderPreferences()
420 {
421 this->isDark = luma(this->renderPreferences.backgroundColor) < 0.25;
422 if (this->gridProgram.has_value())
423 {
424 this->gridProgram->setGridColor(this->isDark ? Qt::white : Qt::black);
425 }
426 }
427
428 void Canvas::setOverpaintCallback(Canvas::OverpaintCallback fn)
429 {
430 this->overpaintCallback = fn;
431 }

mercurial