QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
GeometryImage.cc
Go to the documentation of this file.
1#include "GeometryImage.h"
2
3#include <QtCore/QDir>
4
5#include <array>
6
7using namespace GeometryImage;
8
9static void generateTestGeometries([[maybe_unused]] VehicleGeometryImageProvider &provider)
10{
11#if 0 // enable this to generate a set of test geometry images on startup in the current working directory
12 const QString imagePrefix = "test_geometry_";
13 static bool generatedOnce = false;
14 if (generatedOnce) {
15 return;
16 }
17 generatedOnce = true;
18
19 qWarning() << "Generating Test Geometry Images";
20
21 const QList<ActuatorGeometry> geometries[] = {
22 { // quad
27 },
28 { // off-center quad
33 },
34 { // weird penta
40 },
41 { // octo
50 },
51 { // octo coax
60 },
61 { // hex
68 }
69 };
70
71 const QSize sizes[] = {
72 { 160, 160 },
73 { 120 * 2, 120 },
74 { 160 * 2, 160 }, // default size
75 { 250 * 2, 250 },
76 };
77
78 for (unsigned sizeIdx = 0; sizeIdx < sizeof(sizes) / sizeof(sizes[0]); ++sizeIdx) {
79 const QSize& size = sizes[sizeIdx];
80 for (unsigned geometryIdx = 0; geometryIdx < sizeof(geometries) / sizeof(geometries[0]); ++geometryIdx) {
81 provider.actuators() = geometries[geometryIdx];
82 QPixmap pixmap = provider.requestPixmap("", nullptr, size);
83
84 QString imageFileName = QDir(QDir::currentPath()).filePath(imagePrefix + QString::number(sizeIdx)+"_"
85 +QString::number(geometryIdx)+".png");
86 qWarning() << "Generating image" << imageFileName;
87 QFile file(imageFileName);
88 file.open(QIODevice::WriteOnly);
89 pixmap.save(&file, "PNG");
90 }
91 }
92
93#endif
94}
95
96VehicleGeometryImageProvider::VehicleGeometryImageProvider()
97: QQuickImageProvider(QQuickImageProvider::Pixmap)
98{
100}
101
102void VehicleGeometryImageProvider::drawAxisIndicator(QPainter& p, const QPointF& origin, float fontSize, const QColor& color)
103{
104 const float lineLength = fontSize * 2.f;
105 const float arrowWidth = 6.f;
106 const float arrowHeight = 8.f;
107
108 p.setPen(QPen{color, 1.5f});
109 p.setBrush(color);
110
111 QFont font = p.font();
112 font.setPixelSize(fontSize);
113 p.setFont(font);
114
115 auto drawArrow = [&](const QPointF& start, const QPointF& end) {
116 float arrowLineLength = QLineF{start, end}.length();
117 p.save();
118 p.translate(end);
119 float angle = atan2f(end.y()-start.y(), end.x()-start.x());
120 p.rotate(angle * (180.f / M_PI) + 90.f);
121 p.drawLine(QPointF{0, arrowHeight/2}, QPointF{0, arrowLineLength});
122 QPointF arrow[3] = {
123 QPointF{0.f - arrowWidth/2.f, arrowHeight},
124 QPointF{0.f, 0.f},
125 QPointF{0.f + arrowWidth/2.f, arrowHeight},
126 };
127 p.drawConvexPolygon(arrow, sizeof(arrow) / sizeof(arrow[0]));
128 p.restore();
129 };
130
131 // upwards
132 {
133 QPointF endPos{origin.x(), origin.y() - lineLength};
134 drawArrow(origin, endPos);
135
136 p.save();
137 p.translate(endPos);
138 QRectF textRect{-lineLength, -lineLength, lineLength * 2.f, lineLength};
139 p.drawText(textRect, Qt::AlignHCenter | Qt::AlignBottom, "x");
140 p.restore();
141 }
142
143 // rightwards
144 {
145 QPointF endPos{origin.x() + lineLength, origin.y()};
146 drawArrow(origin, endPos);
147
148 p.save();
149 p.translate(endPos);
150 QRectF textRect{0, -lineLength / 2, lineLength, lineLength};
151 p.drawText(textRect, Qt::AlignLeft | Qt::AlignBaseline, " y");
152 p.restore();
153 }
154}
155
156QPixmap VehicleGeometryImageProvider::requestPixmap([[maybe_unused]] const QString& id, QSize* size, const QSize& requestedSize)
157{
158 // For some reason even though we specify a sourceSize.width/height for the image the requestSize comes through at twice that size.
159 // Because of that we need to remember the requested size and use it later to scale click positions.
160
161 _imageSize = requestedSize;
162 int width = requestedSize.width();
163 int height = requestedSize.height();
164 if (size)
165 *size = QSize(width, height);
166
167 QPixmap pixmap(width, height);
168 pixmap.fill(Qt::transparent);
169
170 _actuatorImagePositions.clear();
171
172 // get the dimensions
173 QVector3D min{1e10f, 1e10f, 1e10f};
174 QVector3D max{-1e10f, -1e10f, -1e10f};
175 for (const auto& actuator : _actuators) {
176 for (int i = 0; i < 3; ++i) {
177 if (actuator.position[i] < min[i]) {
178 min[i] = actuator.position[i];
179 }
180 if (actuator.position[i] > max[i]) {
181 max[i] = actuator.position[i];
182 }
183 }
184 }
185
186 if (_actuators.size() <= 1 || max.x() - min.x() < 0.0001f || max.y() - min.y() < 0.0001f ) {
187 return pixmap;
188 }
189
190 // separate actuators, check for coax (on top of each other)
191 QList<ActuatorGeometry> actuators;
192 QList<ActuatorGeometry> coaxActuators;
193 for (const auto& actuator : _actuators) {
194 if (actuator.type == ActuatorGeometry::Type::Motor) {
195 bool isCoax = false;
196 for (const auto& actuatorBefore : _actuators) {
197 if (actuatorBefore.type == ActuatorGeometry::Type::Motor) {
198 if (&actuatorBefore == &actuator) {
199 break;
200 }
201 QVector2D diff = actuatorBefore.position.toVector2D() - actuator.position.toVector2D();
202 if (diff.length() < 0.03f) {
203 coaxActuators.append(actuator);
204 isCoax = true;
205 break;
206 }
207 }
208 }
209 if (!isCoax) {
210 actuators.append(actuator);
211 }
212 }
213 }
214
215 QPainter p(&pixmap);
216 p.setRenderHint(QPainter::Antialiasing);
217 p.setRenderHint(QPainter::TextAntialiasing);
218
219 const float axisIndicatorSize = 15.f; // font size
220 const float margin = 5.f; // from image borders
221
222 // scaling & center offset
223 float usableWidth = width - 2.f * margin;
224 float usableHeight = height - 2.f * margin;
225 float extraOffsetX = 0.f;
226 // if there's not enough space on the left for the axis to ensure there's no overlap, reduce the usable size
227 if (width < height + axisIndicatorSize * 4.f) {
228 usableWidth -= axisIndicatorSize;
229 usableHeight -= axisIndicatorSize;
230 extraOffsetX = axisIndicatorSize;
231 }
232
233 const float rotorDiameter = std::min(usableWidth, usableHeight) * (_actuators.length() <= 6 ? 0.31f : 0.25f);
234 const float fontSize = rotorDiameter * 0.4f;
235 const float extraYMargin = coaxActuators.length() > 0 ? fontSize * 1.1f : 0.f;
236
237 const float scaleX = (usableWidth - rotorDiameter) / (max.y() - min.y());
238 const float scaleY = (usableHeight - extraYMargin - rotorDiameter) / (max.x() - min.x());
239 const float scale = std::min(scaleX, scaleY);
240 const float offsetX = margin + extraOffsetX + usableWidth / 2.f - (max.y() + min.y()) / 2.f * scale;
241 const float offsetY = margin + (usableHeight - extraYMargin) / 2.f + (max.x() + min.x()) / 2.f * scale;
242
243 // style
244 const QColor clockWiseColor{ 21, 158, 31, 200 };
245 const QColor counterClockWiseColor{ 78, 195, 232, 200 };
246 const QColor frameArrowColor{ 255, 68, 43, 200 };
247 const QColor frameColor{ 150, 150, 150 };
248 const float frameWidth{ 6.f };
249 const float rotorFontSize{ rotorDiameter * 0.4f };
250 const QColor rotorHighlightColor{ frameArrowColor };
251 const QColor fontColor{ _palette.text() };
252
253 auto iterateMotors = [scale, offsetX, offsetY](const QList<ActuatorGeometry> &actuatorsList,
254 std::function<void(const ActuatorGeometry&, QPointF)> draw) {
255 for (const auto& actuator : actuatorsList) {
256 if (actuator.type == ActuatorGeometry::Type::Motor) {
257 QPointF pos{
258 offsetX + actuator.position.y()*scale,
259 offsetY - actuator.position.x()*scale
260 };
261 draw(actuator, pos);
262 }
263 }
264 };
265
266 // draw line from center to actuators first
267 iterateMotors(actuators, [&]([[maybe_unused]] const ActuatorGeometry& actuator, QPointF pos) {
268 p.setPen(QPen{frameColor, frameWidth});
269 p.drawLine(QPointF{offsetX, offsetY}, pos);
270 });
271
272 // frame body
273 p.setPen(frameColor);
274 p.setBrush(QBrush{frameColor});
275 float centerSize = rotorDiameter * 0.8f;
276 p.drawRoundedRect(QRectF{offsetX - centerSize / 2.f, offsetY - centerSize / 2.f, centerSize, centerSize}, frameWidth, frameWidth);
277 p.setPen(frameArrowColor);
278 p.setBrush(frameArrowColor);
279 float arrowWidth = rotorDiameter / 4.f;
280 float arrowHeight = rotorDiameter / 2.f;
281 QPointF arrow[3] = {
282 QPointF{offsetX - arrowWidth / 2.f, offsetY + arrowHeight / 2.f},
283 QPointF{offsetX, offsetY - arrowHeight / 2.f},
284 QPointF{offsetX + arrowWidth / 2.f, offsetY + arrowHeight / 2.f},
285 };
286 p.drawConvexPolygon(arrow, sizeof(arrow) / sizeof(arrow[0]));
287
288 drawAxisIndicator(p, QPointF{axisIndicatorSize / 2.f, height - axisIndicatorSize / 2.f}, axisIndicatorSize, fontColor);
289
290 auto drawMotor = [&](const ActuatorGeometry& actuator, QPointF pos, float yPosOffset, bool labelAtBottom) {
291 p.setPen(Qt::NoPen);
292 QColor arrowColor;
293 if (actuator.spinDirection == ActuatorGeometry::SpinDirection::ClockWise) {
294 p.setBrush(QBrush{clockWiseColor});
295 arrowColor = clockWiseColor;
296 } else {
297 p.setBrush(QBrush{counterClockWiseColor});
298 arrowColor = counterClockWiseColor;
299 }
300 arrowColor.setAlpha(255);
301 if (_palette.globalTheme() == QGCPalette::Light) {
302 arrowColor = arrowColor.darker(200);
303 } else {
304 arrowColor = arrowColor.lighter(130);
305 }
306
307 pos.setY(pos.y() + yPosOffset);
308
309 p.drawEllipse(pos, rotorDiameter/2, rotorDiameter/2);
310
311 QRectF textRect;
312 if (labelAtBottom) {
313 textRect = QRectF{pos.x()-rotorDiameter/2.f, pos.y()+rotorDiameter/2.f-yPosOffset, rotorDiameter, yPosOffset};
314 } else {
315 textRect = QRectF{pos.x()-rotorDiameter/2.f, pos.y()-rotorDiameter/2.f, rotorDiameter, rotorDiameter};
316 }
317 if (actuator.renderOptions.highlight) {
318 p.setPen(rotorHighlightColor);
319 p.setBrush(rotorHighlightColor);
320 float radius;
321 if (labelAtBottom) {
322 radius = rotorFontSize / 2.f;
323 } else {
324 radius = rotorDiameter / 2.f;
325 }
326 p.drawEllipse(textRect.center(), radius, radius);
327 _actuatorImagePositions.append(ImagePosition{actuator.type, actuator.index, textRect.center(), radius});
328 }
329 p.setPen(fontColor);
330 QFont font = p.font();
331 font.setPixelSize(rotorFontSize);
332 p.setFont(font);
333 p.drawText(textRect, Qt::AlignCenter, QString::number(actuator.index + actuator.labelIndexOffset));
334
335 // spin direction arrows
336 int angle = 50;// angle for the whole arc
337 float spinArrowWidth = frameWidth;
338 float spinArrowHeight = frameWidth * 1.25f;
339 float arrowPosition = rotorDiameter / 2.f;
340 p.setPen(QPen{arrowColor, 2.5f});
341 p.setBrush(arrowColor);
342 std::array<int, 2> angleOffsets;
343 if (labelAtBottom) {
344 angleOffsets = {30, 180-30}; // bottom right + left sides
345 } else {
346 angleOffsets = {0, 180}; // right + left sides
347 }
348 for (int angleOffset : angleOffsets) {
349 p.save();
350 p.translate(pos);
351 float ySign = 1.f;
352 if (actuator.spinDirection == ActuatorGeometry::SpinDirection::ClockWise) {
353 p.rotate(angle / 2.f + angleOffset);
354 ySign = -1.f;
355 } else {
356 p.rotate(-angle / 2.f + angleOffset);
357 }
358 QRectF arrowRect{-arrowPosition, -arrowPosition, arrowPosition * 2.f, arrowPosition * 2.f};
359 p.drawArc(arrowRect, 0, -ySign * 16 * angle);
360 QPointF spinArrow[3] = {
361 QPointF{arrowPosition - spinArrowWidth/2.f, ySign*spinArrowHeight/2.f},
362 QPointF{arrowPosition, -ySign*spinArrowHeight/2.f},
363 QPointF{arrowPosition + spinArrowWidth/2.f, ySign*spinArrowHeight/2.f},
364 };
365 p.drawConvexPolygon(spinArrow, sizeof(spinArrow) / sizeof(spinArrow[0]));
366 p.restore();
367 }
368 };
369
370 // draw coax motors
371 iterateMotors(coaxActuators, [&](const ActuatorGeometry& actuator, QPointF pos) {
372 drawMotor(actuator, pos, extraYMargin, true);
373 });
374
375 // draw the rest of the motors
376 iterateMotors(actuators, [&](const ActuatorGeometry& actuator, QPointF pos) {
377 drawMotor(actuator, pos, 0.f, false);
378 });
379
380 return pixmap;
381}
382
384{
385 static VehicleGeometryImageProvider* instance = nullptr;
386 // The instance is managed & deleted by the QML engine
387 if (!instance)
389 return instance;
390}
391
392int VehicleGeometryImageProvider::getHighlightedMotorIndexAtPos(const QSizeF& displaySize, const QPointF &position)
393{
394 // We have to scale the click position to take into account displaySize versus imageSize scaling
395 if (_imageSize.isEmpty()) {
396 qWarning() << "Image size is not set, cannot scale position";
397 return -1;
398 }
399 float scaleX = static_cast<float>(displaySize.width()) / _imageSize.width();
400 float scaleY = static_cast<float>(displaySize.height()) / _imageSize.height();
401 QPointF scaledPosition = QPointF{position.x() / scaleX, position.y() / scaleY};
402
403 int foundIdx = -1;
404 for (int i = 0; i < _actuatorImagePositions.size(); ++i) {
405 if (_actuatorImagePositions[i].type == ActuatorGeometry::Type::Motor) {
406 float radius = _actuatorImagePositions[i].radius;
407 if (QLineF{_actuatorImagePositions[i].position, scaledPosition}.length() < radius) {
408 // in case of multiple matches (overlaps), be safe and do not return any match
409 if (foundIdx != -1) {
410 return -1;
411 }
412 foundIdx = i;
413 }
414 }
415 }
416 if (foundIdx >= 0) {
417 return _actuatorImagePositions[foundIdx].index;
418 }
419 return -1;
420}
421
423{
424 int numMotors = 0;
425 for (const auto& actuator : _actuators) {
426 if (actuator.type == ActuatorGeometry::Type::Motor) {
427 ++numMotors;
428 }
429 }
430 return numMotors;
431}
static void generateTestGeometries(VehicleGeometryImageProvider &provider)
QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override
void drawAxisIndicator(QPainter &p, const QPointF &origin, float fontSize, const QColor &color)
static VehicleGeometryImageProvider * instance()
QList< ActuatorGeometry > & actuators()
int getHighlightedMotorIndexAtPos(const QSizeF &displaySize, const QPointF &position)
static Theme globalTheme()
Definition QGCPalette.h:163