KOSMIndoorMap

painterrenderer.cpp
1/*
2 SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "painterrenderer.h"
8#include "stackblur_p.h"
9#include "render-logging.h"
10
11#include <KOSMIndoorMap/SceneGraph>
12#include <KOSMIndoorMap/View>
13
14#include <QDebug>
15#include <QElapsedTimer>
16#include <QFontMetricsF>
17#include <QGuiApplication>
18#include <QImage>
19#include <QLinearGradient>
20#include <QPainter>
21
22#include <cmath>
23
24using namespace KOSMIndoorMap;
25
26PainterRenderer::PainterRenderer() = default;
27PainterRenderer::~PainterRenderer() = default;
28
29void PainterRenderer::setPainter(QPainter *painter)
30{
31 m_painter = painter;
32}
33
34void PainterRenderer::render(const SceneGraph &sg, View *view)
35{
36 QElapsedTimer frameTimer;
37 frameTimer.start();
38
39 m_view = view;
40 beginRender();
41 renderBackground(sg.backgroundColor());
42
43 for (const auto &layerOffsets : sg.layerOffsets()) {
44 const auto layerBegin = sg.itemsBegin(layerOffsets);
45 const auto layerEnd = sg.itemsEnd(layerOffsets);
46 //qDebug() << "rendering layer" << (*layerBegin)->layer;
47
48 // select elements currently in view
49 m_renderBatch.clear();
50 m_renderBatch.reserve(layerOffsets.second - layerOffsets.first);
51 const QRectF screenRect(QPointF(0, 0), QSizeF(m_view->screenWidth(), m_view->screenHeight()));
52 for (auto it = layerBegin; it != layerEnd; ++it) {
53 if ((*it).payload->inSceneSpace() && m_view->viewport().intersects((*it).payload->boundingRect(view))) {
54 m_renderBatch.push_back((*it).payload.get());
55 }
56 if ((*it).payload->inHUDSpace()) {
57 auto bbox = (*it).payload->boundingRect(view);
58 bbox.moveCenter(m_view->mapSceneToScreen(bbox.center()));
59 if (screenRect.intersects(bbox)) {
60 m_renderBatch.push_back((*it).payload.get());
61 }
62 }
63 }
64
65 for (auto phase : {SceneGraphItemPayload::FillPhase, SceneGraphItemPayload::CasingPhase, SceneGraphItemPayload::StrokePhase, SceneGraphItemPayload::IconPhase, SceneGraphItemPayload::LabelPhase}) {
66 beginPhase(phase);
67 prepareBatch(phase);
68 for (auto it = m_renderBatch.begin(); it != m_renderBatch.end(); ++it) {
69 const auto &item = (*it);
70 if ((item->renderPhases() & phase) == 0) {
71 continue;
72 }
73
74 if (auto i = dynamic_cast<PolygonItem*>(item)) {
75 renderPolygon(i, phase);
76 } else if (auto i = dynamic_cast<MultiPolygonItem*>(item)) {
77 renderMultiPolygon(i, phase);
78 } else if (auto i = dynamic_cast<PolylineItem*>(item)) {
79 renderPolyline(i, phase);
80 } else if (auto i = dynamic_cast<LabelItem*>(item)) {
81 // skip if a higher up item would overlap this one, unless that is explicitly allowed
82 if (phase == SceneGraphItemPayload::IconPhase) {
83 if (!i->iconHidden) {
84 renderLabel(i, phase);
85 }
86 } else if (phase == SceneGraphItemPayload::LabelPhase) {
87 if (!i->textHidden) {
88 renderLabel(i, phase);
89 }
90 }
91 } else {
92 qCritical() << "Unsupported scene graph item!";
93 }
94 }
95 }
96 }
97
98 renderForeground(sg.backgroundColor());
99 endRender();
100 m_view = nullptr;
101
102 qCDebug(RenderLog) << "rendering took:" << frameTimer.elapsed() << "ms for" << sg.items().size() << "items on" << sg.layerOffsets().size() << "layers";
103}
104
105void PainterRenderer::beginRender()
106{
107 m_painter->save();
108}
109
110void PainterRenderer::renderBackground(const QColor &bgColor)
111{
112 m_painter->setTransform(m_view->deviceTransform());
113 m_painter->fillRect(0, 0, m_view->screenWidth(), m_view->screenHeight(), bgColor);
114}
115
116void PainterRenderer::beginPhase(SceneGraphItemPayload::RenderPhase phase)
117{
118 switch (phase) {
119 case SceneGraphItemPayload::NoPhase:
120 Q_UNREACHABLE();
121 case SceneGraphItemPayload::FillPhase:
122 m_painter->setPen(Qt::NoPen);
123 m_painter->setTransform(m_view->sceneToScreenTransform() * m_view->deviceTransform());
124 m_painter->setClipRect(m_view->viewport().intersected(m_view->sceneBoundingBox()));
125 m_painter->setRenderHint(QPainter::Antialiasing, false);
126 break;
127 case SceneGraphItemPayload::CasingPhase:
128 case SceneGraphItemPayload::StrokePhase:
129 m_painter->setBrush(Qt::NoBrush);
130 m_painter->setTransform(m_view->sceneToScreenTransform() * m_view->deviceTransform());
131 m_painter->setClipRect(m_view->viewport().intersected(m_view->sceneBoundingBox()));
132 m_painter->setRenderHint(QPainter::Antialiasing, true);
133 break;
134 case SceneGraphItemPayload::IconPhase:
135 case SceneGraphItemPayload::LabelPhase:
136 m_painter->setTransform(m_view->deviceTransform());
137 m_painter->setRenderHint(QPainter::Antialiasing, true);
139 break;
140 }
141}
142
143void PainterRenderer::prepareBatch(SceneGraphItemPayload::RenderPhase phase)
144{
145 switch (phase) {
146 case SceneGraphItemPayload::NoPhase:
147 Q_UNREACHABLE();
148 case SceneGraphItemPayload::FillPhase:
149 case SceneGraphItemPayload::CasingPhase:
150 case SceneGraphItemPayload::StrokePhase:
151 break;
152 case SceneGraphItemPayload::IconPhase:
153 // place icons/shields starting at the top (== back of m_renderBatch)
154 // and hide everything they would cover (unless overlap is explicitly allowed)
155 // TODO ensure minimum repeat distance between shields here
156 for (auto it = m_renderBatch.rbegin(); it != m_renderBatch.rend(); ++it) {
157 if (((*it)->renderPhases() & SceneGraphItemPayload::IconPhase) == 0) {
158 continue;
159 }
160 const auto item = dynamic_cast<LabelItem*>(*it);
161 if (!item) {
162 continue;
163 }
164 item->iconHidden = false;
165 if (item->allowIconOverlap) {
166 continue;
167 }
168
169 QRectF bbox;
170 if (item->hasShield()) {
171 bbox = item->shieldHitBox(m_view);
172 } else {
173 bbox = item->iconHitBox(m_view);
174 }
175
176 for (auto it2 = it.base(); it2 != m_renderBatch.end(); ++it2) {
177 if (((*it2)->renderPhases() & SceneGraphItemPayload::IconPhase) == 0) {
178 continue;
179 }
180 const auto otherItem = dynamic_cast<LabelItem*>((*it2));
181 if (!otherItem || otherItem->allowIconOverlap) { // TODO remove the allowIconOverlap check, this is a workaround for wrong z order
182 continue;
183 }
184
185 QRectF bbox2;
186 if (otherItem->hasShield()) {
187 bbox2 = otherItem->shieldHitBox(m_view);
188 } else {
189 bbox2 = otherItem->iconHitBox(m_view);
190 }
191
192 if (bbox.intersects(bbox2)) {
193 item->iconHidden = true;
194 break;
195 }
196 }
197 }
198 break;
199 case SceneGraphItemPayload::LabelPhase:
200 // place texts starting at the top (== back of m_renderBatch)
201 // and hide everything they would cover (unless overlap is explicitly allowed)
202 // TODO this doesn't seem to work for line following labels yet
203 // TODO ensure minimum repeat distance between texts here
204 for (auto it = m_renderBatch.rbegin(); it != m_renderBatch.rend(); ++it) {
205 if (((*it)->renderPhases() & SceneGraphItemPayload::LabelPhase) == 0) {
206 continue;
207 }
208 const auto item = dynamic_cast<LabelItem*>(*it);
209 if (!item) {
210 continue;
211 }
212 item->textHidden = false;
213 if (item->allowTextOverlap) {
214 continue;
215 }
216 if (item->iconHidden) {
217 item->textHidden = true; // if the icon is already hidden don't even bother trying text
218 continue;
219 }
220
221 const QRectF bbox = item->textHitBox(m_view);
222
223 // we need to search the full set here
224 // - icons/shields are already rendered, so we can collide with all of those
225 // - non-shield texts are being laid out, so we need to only look at things after a it.base() or later
226 for (auto it2 = m_renderBatch.begin(); it2 != m_renderBatch.end(); ++it2) {
227 if (it2 == std::prev(it.base())) {
228 continue;
229 }
230 const auto p = (*it2)->renderPhases();
231 if ((p & SceneGraphItemPayload::IconPhase) == 0 && ((p & SceneGraphItemPayload::LabelPhase) == 0 || it2 < it.base())) {
232 continue;
233 }
234
235 const auto otherItem = dynamic_cast<LabelItem*>((*it2));
236 if (!otherItem || otherItem->iconHidden || otherItem->allowTextOverlap) { // TODO limit the allowTextOverlap check to icons, this is a workaround for wrong z order
237 continue;
238 }
239
240 if (otherItem->hasShield()) {
241 if (otherItem->shieldHitBox(m_view).intersects(bbox)) {
242 item->textHidden = true;
243 break;
244 }
245 continue;
246 }
247 if (it2 >= it.base() && otherItem->hasText() && !otherItem->textHidden && otherItem->textHitBox(m_view).intersects(bbox)) {
248 item->textHidden = true;
249 break;
250 }
251 if (otherItem->hasIcon() && otherItem->iconHitBox(m_view).intersects(bbox)) {
252 item->textHidden = true;
253 break;
254 }
255 }
256 }
257 break;
258 }
259}
260
261static inline void drawGeometry(QPainter *painter, const QPolygonF &polygon) { painter->drawPolygon(polygon, Qt::OddEvenFill); }
262static inline void drawGeometry(QPainter *painter, const QPainterPath &path) { painter->drawPath(path); }
263
264template <typename T>
265inline void PainterRenderer::renderPolygonFill(PolygonBaseItem *item, const T &geom)
266{
267 if (item->fillBrush.style() != Qt::NoBrush) {
268 m_painter->setBrush(item->fillBrush);
269 drawGeometry(m_painter, geom);
270 }
271 if (item->textureBrush.style() != Qt::NoBrush) {
272 item->textureBrush.setTransform(brushTransform());
273 m_painter->setOpacity(item->textureBrush.color().alphaF());
274 m_painter->setBrush(item->textureBrush);
275 drawGeometry(m_painter, geom);
276 m_painter->setOpacity(1.0);
277 }
278}
279
280template <typename T>
281inline void PainterRenderer::renderPolygonCasing(PolygonBaseItem *item, const T &geom)
282{
283 auto p = item->casingPen;
284 p.setWidthF(mapToSceneWidth(item->casingPen.widthF(), item->casingPenWidthUnit));
285 m_painter->setPen(p);
286 drawGeometry(m_painter, geom);
287}
288
289template <typename T>
290inline void PainterRenderer::renderPolygonLine(PolygonBaseItem *item, const T &geom)
291{
292 auto p = item->pen;
293 p.setWidthF(mapToSceneWidth(item->pen.widthF(), item->penWidthUnit));
294 m_painter->setPen(p);
295 drawGeometry(m_painter, geom);
296}
297
298void PainterRenderer::renderPolygon(PolygonItem *item, SceneGraphItemPayload::RenderPhase phase)
299{
300 if (item->useCasingFillMode()) {
301 if (phase == SceneGraphItemPayload::CasingPhase) {
302 renderPolygonCasing(item, item->polygon);
303 } else if (phase == SceneGraphItemPayload::StrokePhase) {
304 m_painter->setPen(Qt::NoPen);
305 renderPolygonFill(item, item->polygon);
306 m_painter->setBrush(Qt::NoBrush);
307 }
308 } else {
309 if (phase == SceneGraphItemPayload::FillPhase) {
310 renderPolygonFill(item, item->polygon);
311 } else if (phase == SceneGraphItemPayload::StrokePhase) {
312 renderPolygonLine(item, item->polygon);
313 }
314 }
315}
316
317void PainterRenderer::renderMultiPolygon(MultiPolygonItem *item, SceneGraphItemPayload::RenderPhase phase)
318{
319 if (item->useCasingFillMode()) {
320 if (phase == SceneGraphItemPayload::CasingPhase) {
321 renderPolygonCasing(item, item->path);
322 } else if (phase == SceneGraphItemPayload::StrokePhase) {
323 m_painter->setPen(Qt::NoPen);
324 renderPolygonFill(item, item->path);
325 m_painter->setBrush(Qt::NoBrush);
326 }
327 } else {
328 if (phase == SceneGraphItemPayload::FillPhase) {
329 renderPolygonFill(item, item->path);
330 } else if (phase == SceneGraphItemPayload::StrokePhase) {
331 renderPolygonLine(item, item->path);
332 }
333 }
334}
335
336void PainterRenderer::renderPolyline(PolylineItem *item, SceneGraphItemPayload::RenderPhase phase)
337{
338 if (phase == SceneGraphItemPayload::StrokePhase) {
339 auto p = item->pen;
340 p.setWidthF(mapToSceneWidth(item->pen.widthF(), item->penWidthUnit));
341 m_painter->setPen(p);
342 m_painter->drawPolyline(item->path);
343 } else {
344 auto p = item->casingPen;
345 p.setWidthF(mapToSceneWidth(item->pen.widthF(), item->penWidthUnit) + mapToSceneWidth(item->casingPen.widthF(), item->casingPenWidthUnit));
346 m_painter->setPen(p);
347 m_painter->drawPolyline(item->path);
348 }
349}
350
351void PainterRenderer::renderLabel(LabelItem *item, SceneGraphItemPayload::RenderPhase phase)
352{
353 m_painter->save();
354 m_painter->translate(m_view->mapSceneToScreen(item->pos));
355 m_painter->rotate(item->angle);
356
357 auto box = item->boundingRect(m_view);
358 box.moveCenter({0.0, 0.0});
359
360 // compute icon output size
361 QSizeF iconOutputSize = item->iconOutputSize(m_view);
362 if (!item->icon.isNull()) {
363 box.moveTop(-iconOutputSize.height() / 2.0);
364 }
365
366#if 0
367 m_painter->setPen(Qt::green);
368 m_painter->drawRect(item->iconHitBox(m_view).translated(-m_view->mapSceneToScreen(item->pos)));
369 m_painter->setPen(Qt::blue);
370 m_painter->drawRect(item->textHitBox(m_view).translated(-m_view->mapSceneToScreen(item->pos)));
371 m_painter->setPen(Qt::red);
372 m_painter->drawRect(item->shieldHitBox(m_view).translated(-m_view->mapSceneToScreen(item->pos)));
373#endif
374
375 // draw shield
376 // @see https://wiki.openstreetmap.org/wiki/MapCSS/0.2#Shield_properties
377 auto w = item->casingWidth + item->frameWidth + 2.0;
378 if (item->casingWidth > 0.0 && item->casingColor.alpha() > 0) {
379 m_painter->fillRect(box.adjusted(-w, -w, w, w), item->casingColor);
380 }
381 w -= item->casingWidth;
382 if (item->frameWidth > 0.0 && item->frameColor.alpha() > 0) {
383 m_painter->fillRect(box.adjusted(-w, -w, w, w), item->frameColor);
384 }
385 w -= item->frameWidth;
386 if (item->shieldColor.alpha() > 0) {
387 m_painter->fillRect(box.adjusted(-w, -w, w, w), item->shieldColor);
388 }
389
390 // draw icon
391 if (!iconOutputSize.isNull() && phase == SceneGraphItemPayload::IconPhase) {
392 QRectF iconRect(QPointF(-iconOutputSize.width() / 2.0, -iconOutputSize.height() / 2.0), iconOutputSize);
393 m_painter->setOpacity(item->iconOpacity);
394 item->icon.paint(m_painter, iconRect.toRect());
395 m_painter->setOpacity(1.0);
396 }
397 box.moveTop(box.top() + item->textOffset);
398
399 // center-align the text (item->text.size().width() != item->textOutputSize()...)
400 box.setWidth(item->text.size().width());
401 box.moveCenter({0.0, box.center().y()});
402
403 if (item->hasText() && (phase == SceneGraphItemPayload::LabelPhase || item->hasShield())) {
404 // draw text halo
405 if (item->haloRadius > 0.0 && item->haloColor.alphaF() > 0.0) {
406 const auto haloBox = box.adjusted(-item->haloRadius, -item->haloRadius, item->haloRadius, item->haloRadius);
407 QImage haloBuffer(haloBox.size().toSize(), QImage::Format_ARGB32);
408 haloBuffer.fill(Qt::transparent);
409 QPainter haloPainter(&haloBuffer);
410 haloPainter.setPen(item->haloColor);
411 haloPainter.setFont(item->font);
412 auto haloTextRect = box;
413 haloTextRect.moveTopLeft({item->haloRadius, item->haloRadius});
414 if (!item->isComplexText) {
415 haloPainter.drawStaticText(haloTextRect.topLeft(), item->text);
416 } else {
417 haloPainter.drawText(haloTextRect, item->text.text(), item->text.textOption());
418 }
419 StackBlur::blur(haloBuffer, item->haloRadius);
420 haloPainter.setCompositionMode(QPainter::CompositionMode_SourceIn);
421 haloPainter.fillRect(haloBuffer.rect(), item->haloColor);
422 m_painter->drawImage(haloBox, haloBuffer);
423 }
424
425 // draw text
426 m_painter->setPen(item->color);
427 m_painter->setFont(item->font);
428 if (!item->isComplexText) {
429 m_painter->drawStaticText(box.topLeft(), item->text);
430 } else {
431 m_painter->drawText(box, item->text.text(), item->text.textOption());
432 }
433 }
434
435 m_painter->restore();
436}
437
438void PainterRenderer::renderForeground(const QColor &bgColor)
439{
440 // fade out the map at the end of the scene box, to indicate you can't scroll further
441 m_painter->setTransform(m_view->deviceTransform());
442 m_painter->setClipRect(m_view->mapSceneToScreen(m_view->viewport()));
443 const auto borderWidth = 10;
444
445 QColor c(bgColor);
446 c.setAlphaF(0.75);
447 QLinearGradient gradient;
448 gradient.setColorAt(0, bgColor);
449 gradient.setColorAt(0.2, c);
450 gradient.setColorAt(1, Qt::transparent);
451
452 auto r = m_view->mapSceneToScreen(m_view->sceneBoundingBox());
453 r.setBottom(r.top() + borderWidth);
454 gradient.setStart(r.topLeft());
455 gradient.setFinalStop(r.bottomLeft());
456 m_painter->fillRect(r, gradient);
457
458 r = m_view->mapSceneToScreen(m_view->sceneBoundingBox());
459 r.setTop(r.bottom() - borderWidth);
460 gradient.setStart(r.bottomLeft());
461 gradient.setFinalStop(r.topLeft());
462 m_painter->fillRect(r, gradient);
463
464 r = m_view->mapSceneToScreen(m_view->sceneBoundingBox());
465 r.setRight(r.left() + borderWidth);
466 gradient.setStart(r.topLeft());
467 gradient.setFinalStop(r.topRight());
468 m_painter->fillRect(r, gradient);
469
470 r = m_view->mapSceneToScreen(m_view->sceneBoundingBox());
471 r.setLeft(r.right() - borderWidth);
472 gradient.setStart(r.topRight());
473 gradient.setFinalStop(r.topLeft());
474 m_painter->fillRect(r, gradient);
475}
476
477void PainterRenderer::endRender()
478{
479 m_painter->restore();
480}
481
482double PainterRenderer::mapToSceneWidth(double width, Unit unit) const
483{
484 switch (unit) {
485 case Unit::Pixel:
486 return m_view->mapScreenDistanceToSceneDistance(width);
487 case Unit::Meter:
488 return m_view->mapMetersToScene(width);
489 }
490
491 return width;
492}
493
494QTransform PainterRenderer::brushTransform() const
495{
496 // the following is the easy solution here and produces the best quality rendering
497 // m_painter->transform().inverted().translate(m_painter->transform().dx(), m_painter->transform().dy());
498 // the downside however is that it's extremely irritating during continuous scaling
499
500 // the following does basically the same as the above, but with discrete zoom levels
501 // at the expense of rendering quality
502 constexpr const auto TextureZoomSteps = 5.0;
503 auto viewport = m_view->viewportForZoom(std::round(m_view->zoomLevel() * TextureZoomSteps) / TextureZoomSteps,
504 QPointF(m_view->screenWidth() / 2.0, m_view->screenHeight() / 2.0));
505 QTransform t;
506 t.scale(viewport.width() / m_view->screenWidth(), viewport.height() / m_view->screenHeight());
507 t.translate(viewport.x(), viewport.y());
508 return t;
509
510 // TODO maybe the best approach would be the best of both worlds, the second approach
511 // during continuous zooming, the first one when at a fixed zoom level?
512}
A text or icon label.
QRectF boundingRect(const View *view) const override
Bounding box of this item in scene coordinates.
Multi-polygon item, used for polygons with "holes" in them.
Base item for filled polygons.
bool useCasingFillMode() const
Render like lines, ie casing and filling in the stroke phase, rather than the default.
A single filled polygon.
A path/way/line item in the scenegraph.
RenderPhase
See MapCSS spec: "Within a layer, first all fills are rendered, then all casings, then all strokes,...
Scene graph of the currently displayed level.
Definition scenegraph.h:29
QColor backgroundColor() const
Canvas background color.
QRectF viewportForZoom(double zoom, QPointF screenCenter) const
Computes the viewport for the given zoom level and screenCenter.
Definition view.cpp:123
QTransform deviceTransform() const
Device tranformation for manual high DPI scaling.
Definition view.cpp:307
double mapMetersToScene(double meters) const
Returns how many units in scene coordinate represent the distance of meters in the current view trans...
Definition view.cpp:257
int screenWidth() const
Screen-space sizes, ie the size of the on-screen area used for displaying.
Definition view.cpp:63
double mapScreenDistanceToSceneDistance(double distance) const
Converts a distance in screen coordinates to a distance in scene coordinates.
Definition view.cpp:191
QPointF mapSceneToScreen(QPointF scenePos) const
Converts a point in scene coordinates to screen coordinates.
Definition view.cpp:175
QRectF sceneBoundingBox() const
The bounding box of the scene.
Definition view.cpp:142
QTransform sceneToScreenTransform() const
The transformation to apply to scene coordinate to get to the view on screen.
Definition view.cpp:208
OSM-based multi-floor indoor maps for buildings.
Unit
Unit for geometry sizes.
const QColor & color() const const
void setTransform(const QTransform &matrix)
Qt::BrushStyle style() const const
int alpha() const const
float alphaF() const const
qint64 elapsed() const const
void setColorAt(qreal position, const QColor &color)
bool isNull() const const
void paint(QPainter *painter, const QRect &rect, Qt::Alignment alignment, Mode mode, State state) const const
void setFinalStop(const QPointF &stop)
void setStart(const QPointF &start)
CompositionMode_SourceIn
void drawImage(const QPoint &point, const QImage &image)
void drawPath(const QPainterPath &path)
void drawPolygon(const QPoint *points, int pointCount, Qt::FillRule fillRule)
void drawPolyline(const QPoint *points, int pointCount)
void drawRect(const QRect &rectangle)
void drawStaticText(const QPoint &topLeftPosition, const QStaticText &staticText)
void drawText(const QPoint &position, const QString &text)
void fillRect(const QRect &rectangle, QGradient::Preset preset)
void restore()
void rotate(qreal angle)
void save()
void setBrush(Qt::BrushStyle style)
void setClipRect(const QRect &rectangle, Qt::ClipOperation operation)
void setFont(const QFont &font)
void setOpacity(qreal opacity)
void setPen(Qt::PenStyle style)
void setRenderHint(RenderHint hint, bool on)
void setTransform(const QTransform &transform, bool combine)
void translate(const QPoint &offset)
void setWidthF(qreal width)
qreal widthF() const const
QRectF intersected(const QRectF &rectangle) const const
bool intersects(const QRectF &rectangle) const const
void moveCenter(const QPointF &position)
QRectF translated(const QPointF &offset) const const
qreal height() const const
bool isNull() const const
qreal width() const const
QSizeF size() const const
QString text() const const
QTextOption textOption() const const
OddEvenFill
QTransform & scale(qreal sx, qreal sy)
QTransform & translate(qreal dx, qreal dy)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:17:55 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.