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 } |
|