src/layers/edittools.cpp

changeset 263
59b6027b9843
parent 250
2837b549e616
child 264
76a025db4948
equal deleted inserted replaced
262:dc33f8a707c4 263:59b6027b9843
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 "../model.h"
23 #include "../ui/objecteditor.h"
24 #include "../gl/partrenderer.h"
25 #include "../circularprimitive.h"
26 #include "edittools.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 }

mercurial