1 /* |
|
2 * LDForge: LDraw parts authoring CAD |
|
3 * Copyright (C) 2013 - 2020 Teemu Piippo |
|
4 * |
|
5 * This program is free software: you can redistribute it and/or modify |
|
6 * it under the terms of the GNU General Public License as published by |
|
7 * the Free Software Foundation, either version 3 of the License, or |
|
8 * (at your option) any later version. |
|
9 * |
|
10 * This program is distributed in the hope that it will be useful, |
|
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
13 * GNU General Public License for more details. |
|
14 * |
|
15 * You should have received a copy of the GNU General Public License |
|
16 * along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
17 */ |
|
18 |
|
19 #include <QMouseEvent> |
|
20 #include <QPainter> |
|
21 #include "algorithm/earcut.h" |
|
22 #include "document.h" |
|
23 #include "model.h" |
|
24 #include "ui/objecteditor.h" |
|
25 #include "gl/partrenderer.h" |
|
26 #include "circularprimitive.h" |
|
27 |
|
28 // Make mapbox::earcut work with glm::vec3 |
|
29 template<> struct mapbox::util::nth<0, glm::vec3> |
|
30 { |
|
31 static constexpr float get(const glm::vec3& t) { return t.x; } |
|
32 }; |
|
33 |
|
34 template<> struct mapbox::util::nth<1, glm::vec3> |
|
35 { |
|
36 static constexpr float get(const glm::vec3& t) { return t.y; } |
|
37 }; |
|
38 |
|
39 EditTools::EditTools(QObject* parent) : |
|
40 QObject{parent}, |
|
41 RenderLayer{} |
|
42 { |
|
43 } |
|
44 |
|
45 EditTools::~EditTools() |
|
46 { |
|
47 } |
|
48 |
|
49 void EditTools::setEditMode(EditingMode newMode) |
|
50 { |
|
51 this->mode = newMode; |
|
52 } |
|
53 |
|
54 void EditTools::setGridMatrix(const glm::mat4& newGridMatrix) |
|
55 { |
|
56 this->gridMatrix = newGridMatrix; |
|
57 this->gridPlane = planeFromTriangle({ |
|
58 this->gridMatrix * glm::vec4{0, 0, 0, 1}, |
|
59 this->gridMatrix * glm::vec4{1, 0, 0, 1}, |
|
60 this->gridMatrix * glm::vec4{0, 1, 0, 1}, |
|
61 }); |
|
62 } |
|
63 |
|
64 void EditTools::setCircleToolOptions(const CircleToolOptions& options) |
|
65 { |
|
66 this->circleToolOptions = options; |
|
67 } |
|
68 |
|
69 void EditTools::mvpMatrixChanged(const glm::mat4& matrix) |
|
70 { |
|
71 this->mvpMatrix = matrix; |
|
72 } |
|
73 |
|
74 void EditTools::mouseMoved(const QMouseEvent* event) |
|
75 { |
|
76 this->worldPosition = this->renderer->screenToModelCoordinates(event->pos(), this->gridPlane); |
|
77 if (this->worldPosition.has_value()) |
|
78 { |
|
79 // Snap the position to grid. This procedure is basically the "change of basis" and almost follows the |
|
80 // A⁻¹ × M × A formula which is used to perform a transformation in some other coordinate system, except |
|
81 // we actually use the inverted matrix first and the regular one last to perform the transformation of |
|
82 // grid coordinates in our XY coordinate system. Also, we're rounding the coordinates which is obviously |
|
83 // not a linear transformation, but fits the pattern anyway. |
|
84 // First transform the coordinates to the XY plane... |
|
85 this->worldPosition = glm::inverse(this->gridMatrix) * glm::vec4{*this->worldPosition, 1}; |
|
86 // Then round the coordinates to integer precision... |
|
87 this->worldPosition = glm::round(*this->worldPosition); |
|
88 // And finally transform it back to grid coordinates by transforming it with the |
|
89 // grid matrix. |
|
90 this->worldPosition = this->gridMatrix * glm::vec4{*this->worldPosition, 1}; |
|
91 this->polygon.back() = *this->worldPosition; |
|
92 } |
|
93 this->numpoints = this->polygon.size(); |
|
94 if (this->isCloseToExistingPoints()) { |
|
95 this->numpoints -= 1; |
|
96 } |
|
97 } |
|
98 |
|
99 static QVector<QPointF> convertWorldPointsToScreenPoints( |
|
100 const std::vector<glm::vec3> &worldPoints, |
|
101 const PartRenderer* renderer) |
|
102 { |
|
103 QVector<QPointF> points2d; |
|
104 points2d.reserve(static_cast<int>(worldPoints.size())); |
|
105 for (const glm::vec3& point : worldPoints) |
|
106 { |
|
107 points2d.push_back(renderer->modelToScreenCoordinates(point)); |
|
108 } |
|
109 return points2d; |
|
110 } |
|
111 |
|
112 static Winding worldPolygonWinding( |
|
113 const std::vector<glm::vec3> &points, |
|
114 const PartRenderer* renderer) |
|
115 { |
|
116 return winding(QPolygonF{convertWorldPointsToScreenPoints(points, renderer)}); |
|
117 } |
|
118 |
|
119 static void drawWorldPoint( |
|
120 QPainter* painter, |
|
121 const glm::vec3& worldPoint, |
|
122 const PartRenderer* renderer) |
|
123 { |
|
124 const QPointF center = renderer->modelToScreenCoordinates(worldPoint); |
|
125 painter->drawEllipse(inscribe(CircleF{center, 5})); |
|
126 } |
|
127 |
|
128 static void drawWorldPolyline( |
|
129 QPainter *painter, |
|
130 const std::vector<glm::vec3> &points, |
|
131 const PartRenderer* renderer) |
|
132 { |
|
133 painter->drawPolyline(QPolygonF{convertWorldPointsToScreenPoints(points, renderer)}); |
|
134 } |
|
135 |
|
136 static void drawWorldPolygon( |
|
137 QPainter* painter, |
|
138 const std::vector<glm::vec3> &points, |
|
139 const PartRenderer* renderer) |
|
140 { |
|
141 painter->drawPolygon(QPolygonF{convertWorldPointsToScreenPoints(points, renderer)}); |
|
142 } |
|
143 |
|
144 static std::vector<std::vector<glm::vec3>> modelActionPoints(const ModelAction& action) |
|
145 { |
|
146 std::vector<std::vector<glm::vec3>> result; |
|
147 if (const AppendToModel* append = std::get_if<AppendToModel>(&action)) { |
|
148 const ModelElement& newElement = append->newElement; |
|
149 if (const LineSegment* seg = std::get_if<Colored<LineSegment>>(&newElement)) { |
|
150 result.push_back({seg->p1, seg->p2}); |
|
151 } |
|
152 else if (const Triangle* tri = std::get_if<Colored<Triangle>>(&newElement)) { |
|
153 result.push_back({tri->p1, tri->p2, tri->p3}); |
|
154 } |
|
155 else if (const Quadrilateral* quad = std::get_if<Colored<Quadrilateral>>(&newElement)) { |
|
156 result.push_back({quad->p1, quad->p2, quad->p3, quad->p4}); |
|
157 } |
|
158 else if (const CircularPrimitive* circ = std::get_if<Colored<CircularPrimitive>>(&newElement)) { |
|
159 rasterize(*circ, [&](const ModelElement& element){ |
|
160 const auto& subpoints = modelActionPoints(AppendToModel{element}); |
|
161 std::copy(subpoints.begin(), subpoints.end(), std::back_inserter(result)); |
|
162 }); |
|
163 } |
|
164 } |
|
165 return result; |
|
166 } |
|
167 |
|
168 namespace { |
|
169 struct Pens |
|
170 { |
|
171 const QBrush pointBrush; |
|
172 const QPen pointPen; |
|
173 const QPen textPen; |
|
174 const QPen polygonPen; |
|
175 const QPen badPolygonPen; |
|
176 const QBrush greenPolygonBrush; |
|
177 const QBrush redPolygonBrush; |
|
178 }; |
|
179 } |
|
180 |
|
181 static const Pens brightPens{ |
|
182 .pointBrush = {Qt::black}, |
|
183 .pointPen = {QBrush{Qt::black}, 2.0}, |
|
184 .textPen = {Qt::black}, |
|
185 .polygonPen = {QBrush{Qt::black}, 2.0, Qt::DashLine}, |
|
186 .greenPolygonBrush = {QColor{64, 255, 128, 192}}, |
|
187 .redPolygonBrush = {QColor{255, 96, 96, 192}}, |
|
188 }; |
|
189 |
|
190 static const Pens darkPens{ |
|
191 .pointBrush = {Qt::white}, |
|
192 .pointPen = {QBrush{Qt::white}, 2.0}, |
|
193 .textPen = {Qt::white}, |
|
194 .polygonPen = {QBrush{Qt::white}, 2.0, Qt::DashLine}, |
|
195 .greenPolygonBrush = {QColor{64, 255, 128, 192}}, |
|
196 .redPolygonBrush = {QColor{255, 96, 96, 192}}, |
|
197 }; |
|
198 |
|
199 void EditTools::overpaint(QPainter* painter) |
|
200 { |
|
201 painter->save(); |
|
202 const Pens& pens = (this->renderer->isDark() ? darkPens : brightPens); |
|
203 this->renderPreview(painter, &pens); |
|
204 QFont font; |
|
205 font.setBold(true); |
|
206 if (this->worldPosition.has_value()) |
|
207 { |
|
208 painter->setRenderHint(QPainter::Antialiasing); |
|
209 painter->setPen(pens.pointPen); |
|
210 painter->setBrush(pens.greenPolygonBrush); |
|
211 const QPointF pos = this->renderer->modelToScreenCoordinates(*this->worldPosition); |
|
212 painter->drawEllipse(pos, 5, 5); |
|
213 drawBorderedText(painter, pos + QPointF{5, 5}, font, vectorToString(*this->worldPosition)); |
|
214 } |
|
215 painter->restore(); |
|
216 } |
|
217 |
|
218 const std::vector<ModelAction> EditTools::modelActions() const |
|
219 { |
|
220 switch(this->mode) { |
|
221 case SelectMode: |
|
222 return {}; |
|
223 case DrawMode: |
|
224 return drawModeActions(); |
|
225 case CircleMode: |
|
226 return circleModeActions(); |
|
227 } |
|
228 } |
|
229 |
|
230 void EditTools::renderPreview(QPainter* painter, const void* pensptr) |
|
231 { |
|
232 const Pens& pens = *reinterpret_cast<const Pens*>(pensptr); |
|
233 painter->setPen(pens.polygonPen); |
|
234 for (const ModelAction& action : this->modelActions()) { |
|
235 for (const std::vector<glm::vec3>& points : modelActionPoints(action)) { |
|
236 if (points.size() == 2) { |
|
237 drawWorldPolyline(painter, points, renderer); |
|
238 } |
|
239 else { |
|
240 if (worldPolygonWinding(points, this->renderer) == Winding::Clockwise) { |
|
241 painter->setBrush(pens.greenPolygonBrush); |
|
242 } |
|
243 else { |
|
244 painter->setBrush(pens.redPolygonBrush); |
|
245 } |
|
246 drawWorldPolygon(painter, points, this->renderer); |
|
247 } |
|
248 } |
|
249 } |
|
250 painter->setBrush(pens.pointBrush); |
|
251 painter->setPen(pens.pointPen); |
|
252 for (const glm::vec3& point : this->polygon) { |
|
253 drawWorldPoint(painter, point, this->renderer); |
|
254 } |
|
255 } |
|
256 |
|
257 void EditTools::removeLastPoint() |
|
258 { |
|
259 if (this->polygon.size() > 1) { |
|
260 this->polygon.erase(this->polygon.end() - 1); |
|
261 } |
|
262 } |
|
263 |
|
264 bool EditTools::isCloseToExistingPoints() const |
|
265 { |
|
266 if (this->worldPosition.has_value()) { |
|
267 const glm::vec3& pos = *this->worldPosition; |
|
268 return std::any_of(this->polygon.begin(), this->polygon.end() - 1, [&pos](const glm::vec3& p){ |
|
269 return isclose(pos, p); |
|
270 }); |
|
271 } |
|
272 else { |
|
273 return false; |
|
274 } |
|
275 } |
|
276 |
|
277 EditingMode EditTools::currentEditingMode() const |
|
278 { |
|
279 return this->mode; |
|
280 } |
|
281 |
|
282 void EditTools::mouseClick(const QMouseEvent* event) |
|
283 { |
|
284 switch(this->mode) { |
|
285 case SelectMode: |
|
286 if (event->button() == Qt::LeftButton) { |
|
287 const ModelId highlighted = this->renderer->pick(event->pos()); |
|
288 Q_EMIT this->select({highlighted}, false); |
|
289 } |
|
290 break; |
|
291 case DrawMode: |
|
292 if (event->button() == Qt::LeftButton and this->worldPosition.has_value()) { |
|
293 if (isCloseToExistingPoints()) { |
|
294 this->closeShape(); |
|
295 } |
|
296 else { |
|
297 this->polygon.push_back(*this->worldPosition); |
|
298 } |
|
299 } |
|
300 break; |
|
301 case CircleMode: |
|
302 if (event->button() == Qt::LeftButton and this->worldPosition.has_value()) { |
|
303 if (this->polygon.size() == 2) { |
|
304 this->closeShape(); |
|
305 } |
|
306 else { |
|
307 this->polygon.push_back(*this->worldPosition); |
|
308 } |
|
309 } |
|
310 break; |
|
311 } |
|
312 if (event->button() == Qt::RightButton and this->polygon.size() > 1) { |
|
313 this->removeLastPoint(); |
|
314 } |
|
315 } |
|
316 |
|
317 struct MergedTriangles |
|
318 { |
|
319 std::vector<Quadrilateral> quadrilaterals; |
|
320 std::set<std::size_t> cutTriangles; |
|
321 }; |
|
322 |
|
323 static MergedTriangles mergeTriangles( |
|
324 const std::vector<std::uint16_t>& indices, |
|
325 const std::vector<glm::vec3>& polygon) |
|
326 { |
|
327 MergedTriangles result; |
|
328 using indextype = std::uint16_t; |
|
329 using indexpair = std::pair<indextype, indextype>; |
|
330 struct boundaryinfo { indextype third; std::size_t triangleid; }; |
|
331 std::map<indexpair, boundaryinfo> boundaries; |
|
332 for (std::size_t i = 0; i < indices.size(); i += 3) { |
|
333 const auto add = [&](const std::size_t o1, const std::size_t o2, const std::size_t o3){ |
|
334 const auto key = std::make_pair(indices[i + o1], indices[i + o2]); |
|
335 boundaries[key] = {indices[i + o3], i}; |
|
336 }; |
|
337 add(0, 1, 2); |
|
338 add(1, 2, 0); |
|
339 add(2, 0, 1); |
|
340 } |
|
341 std::vector<std::array<indextype, 4>> quadindices; |
|
342 std::vector<Quadrilateral> quads; |
|
343 bool repeat = true; |
|
344 const auto iscut = [&result](const std::size_t i){ |
|
345 return result.cutTriangles.find(i) != result.cutTriangles.end(); |
|
346 }; |
|
347 while (repeat) { |
|
348 repeat = false; |
|
349 // Go through triangle boundaries |
|
350 for (const auto& it1 : boundaries) { |
|
351 const indexpair& pair1 = it1.first; |
|
352 const boundaryinfo& boundary1 = it1.second; |
|
353 // .. the ones we haven't already merged anyway |
|
354 if (not iscut(boundary1.triangleid)) { |
|
355 // Look for its inverse boundary to find the touching triangle |
|
356 const auto pair2 = std::make_pair(pair1.second, pair1.first); |
|
357 const auto it2 = boundaries.find(pair2); |
|
358 // Also if that hasn't been cut |
|
359 if (it2 != boundaries.end() and not iscut(it2->second.triangleid)) { |
|
360 const Quadrilateral quad{ |
|
361 polygon[pair1.first], |
|
362 polygon[it2->second.third], |
|
363 polygon[pair1.second], |
|
364 polygon[boundary1.third], |
|
365 }; |
|
366 if (isConvex(quad)) { |
|
367 result.quadrilaterals.push_back(quad); |
|
368 result.cutTriangles.insert(boundary1.triangleid); |
|
369 result.cutTriangles.insert(it2->second.triangleid); |
|
370 repeat = true; |
|
371 } |
|
372 } |
|
373 } |
|
374 } |
|
375 } |
|
376 return result; |
|
377 } |
|
378 |
|
379 |
|
380 const std::vector<ModelAction> EditTools::circleModeActions() const |
|
381 { |
|
382 std::vector<ModelAction> result; |
|
383 if (this->numpoints == 2) { |
|
384 const glm::vec3 x = polygon[1] - polygon[0]; |
|
385 glm::mat4 transform{ |
|
386 glm::vec4{x, 0}, |
|
387 this->gridMatrix[2], |
|
388 glm::vec4{glm::cross(glm::vec3{-this->gridMatrix[2]}, x), 0}, |
|
389 glm::vec4{this->polygon[0], 1}, |
|
390 }; |
|
391 Colored<CircularPrimitive> circ{ |
|
392 CircularPrimitive{ |
|
393 .type = this->circleToolOptions.type, |
|
394 .fraction = this->circleToolOptions.fraction, |
|
395 .transformation = transform, |
|
396 }, |
|
397 MAIN_COLOR |
|
398 }; |
|
399 result.push_back(AppendToModel{.newElement = circ}); |
|
400 } |
|
401 return result; |
|
402 } |
|
403 |
|
404 const std::vector<ModelAction> EditTools::drawModeActions() const |
|
405 { |
|
406 std::vector<ModelAction> result; |
|
407 if (this->numpoints == 2) { |
|
408 result.push_back(AppendToModel{ |
|
409 .newElement = Colored<LineSegment>{ |
|
410 LineSegment{ |
|
411 .p1 = this->polygon[0], |
|
412 .p2 = this->polygon[1], |
|
413 }, |
|
414 EDGE_COLOR, |
|
415 } |
|
416 }); |
|
417 } |
|
418 else if (this->numpoints > 2) { |
|
419 const glm::mat4 inverseGrid = glm::inverse(this->gridMatrix); |
|
420 std::vector<std::vector<glm::vec3>> polygons{1}; |
|
421 std::vector<glm::vec3>& polygon2d = polygons.back(); |
|
422 polygon2d.reserve(this->numpoints); |
|
423 for (std::size_t i = 0; i < this->numpoints; ++i) { |
|
424 polygon2d.push_back(inverseGrid * glm::vec4{this->polygon[i], 1}); |
|
425 } |
|
426 using indextype = std::uint16_t; |
|
427 const std::vector<indextype> indices = mapbox::earcut<std::uint16_t>(polygons); |
|
428 MergedTriangles mergedTriangles = mergeTriangles(indices, this->polygon); |
|
429 for (const Quadrilateral& quad : mergedTriangles.quadrilaterals) { |
|
430 result.push_back(AppendToModel{ |
|
431 .newElement = Colored<Quadrilateral>{quad, MAIN_COLOR}, |
|
432 }); |
|
433 } |
|
434 for (std::size_t i = 0; i < indices.size(); i += 3) { |
|
435 if (mergedTriangles.cutTriangles.find(i) == mergedTriangles.cutTriangles.end()) { |
|
436 result.push_back(AppendToModel{ |
|
437 .newElement = Colored<Triangle>{ |
|
438 Triangle{ |
|
439 .p1 = this->polygon[indices[i]], |
|
440 .p2 = this->polygon[indices[i + 1]], |
|
441 .p3 = this->polygon[indices[i + 2]], |
|
442 }, |
|
443 MAIN_COLOR, |
|
444 } |
|
445 }); |
|
446 } |
|
447 } |
|
448 } |
|
449 return result; |
|
450 } |
|
451 |
|
452 void EditTools::closeShape() |
|
453 { |
|
454 for (const ModelAction& action : this->modelActions()) { |
|
455 Q_EMIT this->modelAction(action); |
|
456 } |
|
457 this->polygon.clear(); |
|
458 this->polygon.push_back(this->worldPosition.value_or(glm::vec3{0, 0, 0})); |
|
459 } |
|