1 | // Copyright (C) 2024 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
3 | |
4 | #include "qquick3dxritem_p.h" |
5 | #include "qquick3dxrview_p.h" |
6 | #include <QtQuick3D/private/qquick3dnode_p_p.h> |
7 | #include <QtQuick/private/qquickrectangle_p.h> |
8 | #include <QColor> |
9 | |
10 | QT_BEGIN_NAMESPACE |
11 | |
12 | |
13 | class QQuick3DXrItemPrivate : public QQuick3DNodePrivate |
14 | { |
15 | Q_DECLARE_PUBLIC(QQuick3DXrItem) |
16 | public: |
17 | QQuick3DXrItemPrivate() : QQuick3DNodePrivate(QQuick3DXrItemPrivate::Type::Node) |
18 | { |
19 | } |
20 | |
21 | void setContentItem(QQuickItem *newContentItem) |
22 | { |
23 | Q_Q(QQuick3DXrItem); |
24 | m_contentItem = newContentItem; |
25 | |
26 | initParentItem(); |
27 | |
28 | if (m_contentItemDestroyedConnection) { |
29 | QObject::disconnect(m_contentItemDestroyedConnection); |
30 | m_contentItemDestroyedConnection = {}; |
31 | } |
32 | if (m_contentItem) { |
33 | m_contentItem->setParentItem(m_containerItem); |
34 | m_contentItem->setParent(m_containerItem); |
35 | m_contentItemDestroyedConnection = QObject::connect(sender: m_contentItem, signal: &QObject::destroyed, context: q, slot: [q]() { |
36 | q->setContentItem(nullptr); |
37 | }); |
38 | |
39 | if (m_automaticHeight) { |
40 | setAutomaticHeightConnection(); |
41 | } |
42 | |
43 | if (m_automaticWidth) { |
44 | setAutomaticWidthConnection(); |
45 | } |
46 | } |
47 | |
48 | updateContent(); |
49 | } |
50 | |
51 | void initParentItem() |
52 | { |
53 | Q_Q(QQuick3DXrItem); |
54 | if (!m_containerItem) { |
55 | m_containerItem = new QQuickRectangle; |
56 | m_containerItem->setTransformOrigin(QQuickItem::TopLeft); |
57 | m_containerItem->setParent(q); |
58 | m_containerItem->setVisible(q->visible()); |
59 | QObject::connect(sender: q, signal: &QQuick3DNode::visibleChanged, context: m_containerItem, slot: [this, q](){ |
60 | m_containerItem->setVisible(q->visible()); |
61 | }); |
62 | |
63 | auto dataProp = data(); |
64 | dataProp.append(&dataProp, m_containerItem); |
65 | } |
66 | } |
67 | |
68 | void updateContent(); |
69 | |
70 | void setAutomaticHeightConnection() |
71 | { |
72 | Q_Q(QQuick3DXrItem); |
73 | if (m_automaticHeightConnection && ((m_contentItem == nullptr) || !m_automaticHeight)) { |
74 | QObject::disconnect(m_automaticHeightConnection); |
75 | m_automaticHeightConnection = {}; |
76 | } |
77 | if (m_contentItem) { |
78 | m_automaticHeightConnection = QObject::connect(sender: m_contentItem, signal: &QQuickItem::heightChanged, context: q, slot: [this, q](){ |
79 | qreal newHeight = m_contentItem->height()/m_pixelsPerUnit; |
80 | if (m_height != newHeight) { |
81 | m_height = newHeight; |
82 | emit q->heightChanged(); |
83 | updateContent(); |
84 | } |
85 | }); |
86 | } |
87 | } |
88 | |
89 | void setAutomaticWidthConnection() |
90 | { |
91 | Q_Q(QQuick3DXrItem); |
92 | if (m_automaticWidthConnection && ((m_contentItem == nullptr) || !m_automaticHeight)) { |
93 | QObject::disconnect(m_automaticWidthConnection); |
94 | m_automaticWidthConnection = {}; |
95 | } |
96 | |
97 | if (m_contentItem) { |
98 | m_automaticWidthConnection = QObject::connect(sender: m_contentItem, signal: &QQuickItem::widthChanged, context: q, slot: [this, q](){ |
99 | qreal newWidth = m_contentItem->width()/m_pixelsPerUnit; |
100 | if (m_width != newWidth) { |
101 | m_width = newWidth; |
102 | emit q->widthChanged(); |
103 | updateContent(); |
104 | } |
105 | }); |
106 | } |
107 | } |
108 | |
109 | QQuickItem *m_contentItem = nullptr; |
110 | QQuickRectangle *m_containerItem = nullptr; |
111 | QPointer<QQuick3DXrView> m_XrView; |
112 | QMetaObject::Connection m_contentItemDestroyedConnection; |
113 | QMetaObject::Connection m_automaticHeightConnection; |
114 | QMetaObject::Connection m_automaticWidthConnection; |
115 | QColor m_color = Qt::white; |
116 | qreal m_pixelsPerUnit { 1.0 }; |
117 | qreal m_width = 1; |
118 | qreal m_height = 1; |
119 | bool m_manualPixelsPerUnit = false; |
120 | bool m_automaticHeight = false; |
121 | bool m_automaticWidth = false; |
122 | }; |
123 | |
124 | static inline qreal calculatePPU(qreal pxWidth, qreal pxHeight, qreal diagonal) |
125 | { |
126 | return (diagonal > 0) ? std::sqrt(x: (pxWidth * pxWidth) + (pxHeight * pxHeight)) / diagonal : 1.0; |
127 | } |
128 | |
129 | void QQuick3DXrItemPrivate::updateContent() |
130 | { |
131 | if (!componentComplete) |
132 | return; |
133 | Q_Q(QQuick3DXrItem); |
134 | initParentItem(); |
135 | m_containerItem->setColor(m_color); |
136 | if (m_contentItem) { |
137 | if (Q_UNLIKELY(m_manualPixelsPerUnit && m_pixelsPerUnit < 0)) { |
138 | qWarning() << "XrItem invalid pixelPerUnit" << m_pixelsPerUnit; |
139 | return; |
140 | } |
141 | qreal newScale; |
142 | if (m_manualPixelsPerUnit) { |
143 | newScale = 1.0 / m_pixelsPerUnit; |
144 | |
145 | } else { |
146 | qreal diagonal = std::sqrt(x: (m_width * m_width) + (m_height * m_height)); |
147 | qreal ppu = calculatePPU(pxWidth: m_contentItem->width(), pxHeight: m_contentItem->height(), diagonal); |
148 | if (ppu <= 0) |
149 | ppu = 1.0; |
150 | q->setPixelsPerUnit(ppu); |
151 | newScale = 1.0 / ppu; |
152 | } |
153 | QSizeF newSize(m_width / newScale, m_height / newScale); |
154 | m_containerItem->setSize(newSize); |
155 | m_containerItem->setScale(newScale); |
156 | } |
157 | } |
158 | |
159 | /*! |
160 | \qmltype XrItem |
161 | \inqmlmodule QtQuick3D.Xr |
162 | \inherits Node |
163 | \brief A virtual surface in 3D space that can hold 2D user interface content. |
164 | \since 6.8 |
165 | |
166 | The XrItem type is a Qt Quick 3D \l Node that represents a rectangle with \l width and \l height. |
167 | It holds one Qt Quick \l Item, specified by \l contentItem, and scales it to fit. |
168 | This gives a convenient way to take traditional 2D user interfaces and display them on a virtual surface that has |
169 | a real world size. |
170 | |
171 | Any other children of the XrItem will be treated as normal children of a Node, and will not be scaled. |
172 | |
173 | For example the following code will create a virtual surface that's 1 meter by 1 meter and with a content item |
174 | that's 600 pixels by 600 pixels. Note that the effect here is achieved by scaling the content item and not |
175 | by rendering the content item at a higher resolution. |
176 | |
177 | \code |
178 | XrItem { |
179 | width: 100 |
180 | height: 100 |
181 | contentItem: Rectangle { |
182 | width: 600 |
183 | height: 600 |
184 | color: "red" |
185 | } |
186 | } |
187 | \endcode |
188 | */ |
189 | QQuick3DXrItem::QQuick3DXrItem(QQuick3DNode *parent) |
190 | : QQuick3DNode(*(new QQuick3DXrItemPrivate()), parent) |
191 | { |
192 | } |
193 | |
194 | QQuick3DXrItem::~QQuick3DXrItem() |
195 | { |
196 | Q_D(QQuick3DXrItem); |
197 | if (d->m_XrView) |
198 | d->m_XrView->unregisterXrItem(xrItem: this); |
199 | } |
200 | |
201 | void QQuick3DXrItem::componentComplete() |
202 | { |
203 | Q_D(QQuick3DXrItem); |
204 | QQuick3DNode::componentComplete(); // Sets d->componentComplete, so must be called first |
205 | |
206 | auto findView = [this]() -> QQuick3DXrView * { |
207 | QQuick3DNode *parent = parentNode(); |
208 | while (parent) { |
209 | if (auto *xrView = qobject_cast<QQuick3DXrView*>(object: parent)) |
210 | return xrView; |
211 | parent = parent->parentNode(); |
212 | } |
213 | return nullptr; |
214 | }; |
215 | d->m_XrView = findView(); |
216 | if (d->m_XrView) |
217 | d->m_XrView->registerXrItem(newXrItem: this); |
218 | else |
219 | qWarning(msg: "Could not find XrView for XrItem" ); |
220 | d->updateContent(); |
221 | } |
222 | |
223 | /*! |
224 | \qmlproperty Item XrItem::contentItem |
225 | |
226 | This property holds the content item that will be displayed on the virtual surface. |
227 | The content item's size will be used to calculate the pixels per unit value and scale based on this item's size. |
228 | |
229 | \sa pixelsPerUnit |
230 | */ |
231 | QQuickItem *QQuick3DXrItem::contentItem() const |
232 | { |
233 | Q_D(const QQuick3DXrItem); |
234 | return d->m_contentItem; |
235 | } |
236 | |
237 | void QQuick3DXrItem::setContentItem(QQuickItem *newContentItem) |
238 | { |
239 | Q_D(QQuick3DXrItem); |
240 | if (d->m_contentItem == newContentItem) |
241 | return; |
242 | |
243 | d->setContentItem(newContentItem); |
244 | emit contentItemChanged(); |
245 | } |
246 | |
247 | /*! |
248 | \qmlproperty real XrItem::pixelsPerUnit |
249 | |
250 | This property determines how many pixels in the contentItems's 2D coordinate system will fit |
251 | in one unit of the XrItem's 3D coordinate system. By default, this is calculated based on the |
252 | content item's size and the size of the XrItem. |
253 | |
254 | Set \l manualPixelsPerUnit to disable the default behavior and set the pixels per unit value manually. |
255 | |
256 | \sa manualPixelsPerUnit |
257 | */ |
258 | qreal QQuick3DXrItem::pixelsPerUnit() const |
259 | { |
260 | Q_D(const QQuick3DXrItem); |
261 | return d->m_pixelsPerUnit; |
262 | } |
263 | |
264 | void QQuick3DXrItem::setPixelsPerUnit(qreal newPixelsPerUnit) |
265 | { |
266 | Q_D(QQuick3DXrItem); |
267 | if (qFuzzyCompare(p1: d->m_pixelsPerUnit, p2: newPixelsPerUnit)) |
268 | return; |
269 | |
270 | d->m_pixelsPerUnit = newPixelsPerUnit; |
271 | |
272 | if (d->m_manualPixelsPerUnit) |
273 | d->updateContent(); |
274 | |
275 | emit pixelsPerUnitChanged(); |
276 | } |
277 | |
278 | /*! |
279 | \qmlproperty bool XrItem::manualPixelsPerUnit |
280 | |
281 | If this property is \c true, the ratio between the contentItems's 2D coordinate system and this |
282 | XrItem's 3D coordinate system is defined by the value of \l pixelsPerUnit. If this property is \c false, |
283 | the ratio is calculated based on the content item's size and the size of the XrItem. |
284 | |
285 | \default false |
286 | \sa pixelsPerUnit |
287 | */ |
288 | |
289 | bool QQuick3DXrItem::manualPixelsPerUnit() const |
290 | { |
291 | Q_D(const QQuick3DXrItem); |
292 | return d->m_manualPixelsPerUnit; |
293 | } |
294 | |
295 | void QQuick3DXrItem::setManualPixelsPerUnit(bool newManualPixelsPerUnit) |
296 | { |
297 | Q_D(QQuick3DXrItem); |
298 | if (d->m_manualPixelsPerUnit == newManualPixelsPerUnit) |
299 | return; |
300 | d->m_manualPixelsPerUnit = newManualPixelsPerUnit; |
301 | d->updateContent(); |
302 | emit manualPixelsPerUnitChanged(); |
303 | } |
304 | |
305 | /*! |
306 | \qmlproperty real XrItem::width |
307 | |
308 | This property defines the width of the XrItem in the 3D coordinate system. |
309 | \sa height |
310 | */ |
311 | |
312 | qreal QQuick3DXrItem::width() const |
313 | { |
314 | Q_D(const QQuick3DXrItem); |
315 | return d->m_width; |
316 | } |
317 | |
318 | void QQuick3DXrItem::setWidth(qreal newWidth) |
319 | { |
320 | Q_D(QQuick3DXrItem); |
321 | if ((d->m_width == newWidth) || d->m_automaticWidth) |
322 | return; |
323 | d->m_width = newWidth; |
324 | emit widthChanged(); |
325 | |
326 | d->updateContent(); |
327 | } |
328 | |
329 | /*! |
330 | \qmlproperty real XrItem::height |
331 | |
332 | This property defines the height of the XrItem in the 3D coordinate system. |
333 | \sa width |
334 | */ |
335 | |
336 | qreal QQuick3DXrItem::height() const |
337 | { |
338 | Q_D(const QQuick3DXrItem); |
339 | return d->m_height; |
340 | } |
341 | |
342 | void QQuick3DXrItem::setHeight(qreal newHeight) |
343 | { |
344 | Q_D(QQuick3DXrItem); |
345 | if ((d->m_height == newHeight) || d->m_automaticHeight) |
346 | return; |
347 | d->m_height = newHeight; |
348 | emit heightChanged(); |
349 | |
350 | d->updateContent(); |
351 | } |
352 | |
353 | // Sends appropriate touch events. |
354 | // Updates the touchState and returns true if this item grabs the touch point. |
355 | // touchState is input/output, and input contains the previous state if touchState->grabbed is true |
356 | |
357 | bool QQuick3DXrItem::handleVirtualTouch(QQuick3DXrView *view, const QVector3D &pos, TouchState *touchState, QVector3D *offset) |
358 | { |
359 | Q_D(QQuick3DXrItem); |
360 | |
361 | auto mappedPos = mapPositionFromScene(scenePosition: pos); |
362 | |
363 | QPointF point = {mappedPos.x(), -mappedPos.y()}; |
364 | |
365 | constexpr qreal sideMargin = 20; // How far outside the rect do you have to go to cancel the grab (cm) |
366 | constexpr qreal cancelDepth = 50; // How far through the rect do you have to go to cancel the grab (cm) |
367 | constexpr qreal hoverHeight = 10; // How far above does the hover state begin (cm). NOTE: no hover events actually sent |
368 | |
369 | constexpr qreal releaseHeight = 2; // How far to move towards/from the surface to count as a press/release when below |
370 | constexpr qreal smallDistance = 0.5; // Any movement shorter than this distance is ignored for press/release below the surface |
371 | constexpr qreal longDistance = 5; // Any in-surface movement larger than this distance means this is not a press/release below the surface |
372 | constexpr int releaseTime = 500; // How fast does the finger have to move to count as press/release below the surface |
373 | constexpr qreal releaseHeightSquared = releaseHeight * releaseHeight; |
374 | constexpr qreal smallDistanceSquared = smallDistance * smallDistance; |
375 | constexpr qreal longDistanceSquared = longDistance * longDistance; |
376 | |
377 | const float z = mappedPos.z(); |
378 | |
379 | const bool wayOutside = point.x() < -sideMargin || point.x() > width() + sideMargin |
380 | || point.y() < -sideMargin || point.y() > height() + sideMargin || z < -cancelDepth; |
381 | const bool inside = point.x() >= 0 && point.x() <= width() && point.y() >= 0 && point.y() <= height() && !wayOutside; |
382 | |
383 | const bool wasGrabbed = touchState->grabbed; |
384 | const bool wasPressed = touchState->pressed; |
385 | |
386 | bool hover = z > 0 && z < hoverHeight; |
387 | |
388 | bool pressed = false; |
389 | bool grab; |
390 | bool resetPrevious = false; |
391 | qint64 now = QDateTime::currentMSecsSinceEpoch(); |
392 | |
393 | if (wasGrabbed) { |
394 | // We maintain a grab as long as we don't move too far away while below the surface, or if we hover inside |
395 | |
396 | // We release if we go from below to above, or if we move upwards fast enough |
397 | // We press if we go from above to below, or if we push downwards fast enough |
398 | // We maintain press otherwise |
399 | // If we move outside when pressed, we should maintain pressed state, but send release event |
400 | |
401 | QVector3D distFromPrev = mappedPos - touchState->previous; |
402 | qint64 msSincePrev = now - touchState->timestamp; |
403 | |
404 | const qreal prevZ = touchState->previous.z(); |
405 | |
406 | if (prevZ > 0 && z <= 0) { |
407 | // New press from above |
408 | pressed = true; |
409 | resetPrevious = true; |
410 | } else if (msSincePrev > releaseTime || z > 0) { |
411 | resetPrevious = true; |
412 | // If the timestamp of the last significant move is older than the cutoff, we maintain the press state if we're below the surface |
413 | // We're never pressed if we're above the surface |
414 | pressed = z <= 0 && wasPressed; |
415 | } else { |
416 | // We know we're within the cutoff interval, and below the surface. |
417 | const qreal hDistSquared = distFromPrev.x() * distFromPrev.x() + distFromPrev.y() * distFromPrev.y(); |
418 | const qreal vDistSquared = distFromPrev.z() * distFromPrev.z(); |
419 | const qreal distSquared = hDistSquared + vDistSquared; |
420 | |
421 | if (distSquared < smallDistanceSquared) { |
422 | // Ignore the movement if it's small. |
423 | resetPrevious = false; |
424 | pressed = wasPressed; |
425 | } else if (hDistSquared > longDistanceSquared) { |
426 | // It's not a press/release if it's a long move inside the surface |
427 | resetPrevious = true; |
428 | pressed = wasPressed; |
429 | } else if (vDistSquared > releaseHeightSquared) { |
430 | // Significant vertical move |
431 | resetPrevious = true; |
432 | pressed = distFromPrev.z() < 0; |
433 | } else { |
434 | resetPrevious = false; |
435 | pressed = wasPressed; |
436 | } |
437 | } |
438 | |
439 | grab = (z <= 0 && !wayOutside) || (hover && inside); |
440 | } else { |
441 | // We don't want movement behind the surface to register as pressed, so we need at least one hover before a press |
442 | grab = hover && inside; |
443 | resetPrevious = true; |
444 | } |
445 | |
446 | if (grab) { |
447 | float zOffset = qMin(a: z, b: 0.0); |
448 | if (offset) |
449 | *offset = sceneRotation() * QVector3D{ 0, 0, -zOffset }; |
450 | touchState->grabbed = true; |
451 | touchState->target = this; |
452 | touchState->touchDistance = z; |
453 | touchState->pressed = pressed; |
454 | touchState->cursorPos = point; |
455 | if (resetPrevious) { |
456 | touchState->previous = mappedPos; |
457 | touchState->timestamp = now; |
458 | } |
459 | view->setTouchpoint(target: d->m_containerItem, position: point, pointId: touchState->pointId, active: pressed && inside); // pressed state maintained outside, but release/press events must be sent when leave/enter |
460 | return true; |
461 | } |
462 | |
463 | if (wasGrabbed) { |
464 | touchState->grabbed = false; |
465 | touchState->target = nullptr; |
466 | view->setTouchpoint(target: d->m_containerItem, position: point, pointId: touchState->pointId, active: false); |
467 | } |
468 | |
469 | return false; |
470 | } |
471 | |
472 | /*! |
473 | \qmlproperty color XrItem::color |
474 | |
475 | This property defines the background color of the XrItem. |
476 | |
477 | \default "white" |
478 | */ |
479 | |
480 | QColor QQuick3DXrItem::color() const |
481 | { |
482 | Q_D(const QQuick3DXrItem); |
483 | return d->m_color; |
484 | } |
485 | |
486 | void QQuick3DXrItem::setColor(const QColor &newColor) |
487 | { |
488 | Q_D(QQuick3DXrItem); |
489 | if (d->m_color == newColor) |
490 | return; |
491 | d->m_color = newColor; |
492 | emit colorChanged(); |
493 | d->updateContent(); |
494 | } |
495 | |
496 | /*! |
497 | \qmlproperty bool XrItem::automaticHeight |
498 | |
499 | When set to true XrItem will ignore height set through height property and use height calculated from contentItem height. |
500 | |
501 | \default "false" |
502 | \sa automaticWidth |
503 | */ |
504 | |
505 | bool QQuick3DXrItem::automaticHeight() const |
506 | { |
507 | Q_D(const QQuick3DXrItem); |
508 | return d->m_automaticHeight; |
509 | } |
510 | |
511 | void QQuick3DXrItem::setAutomaticHeight(bool newAutomaticHeight) |
512 | { |
513 | Q_D(QQuick3DXrItem); |
514 | if (d->m_automaticHeight == newAutomaticHeight) { |
515 | return; |
516 | } |
517 | |
518 | d->m_automaticHeight = newAutomaticHeight; |
519 | d->setAutomaticHeightConnection(); |
520 | d->updateContent(); |
521 | emit automaticHeightChanged(); |
522 | } |
523 | |
524 | /*! |
525 | \qmlproperty bool XrItem::automaticWidth |
526 | |
527 | When set to true XrItem will ignore width set through width property and use width calculated from contentItem width. |
528 | |
529 | \default "false" |
530 | \sa automaticHeight |
531 | */ |
532 | |
533 | bool QQuick3DXrItem::automaticWidth() const |
534 | { |
535 | Q_D(const QQuick3DXrItem); |
536 | return d->m_automaticWidth; |
537 | } |
538 | |
539 | void QQuick3DXrItem::setAutomaticWidth(bool newAutomaticWidth) |
540 | { |
541 | Q_D(QQuick3DXrItem); |
542 | if (d->m_automaticWidth == newAutomaticWidth) |
543 | return; |
544 | |
545 | d->m_automaticWidth = newAutomaticWidth; |
546 | d->setAutomaticWidthConnection(); |
547 | d->updateContent(); |
548 | emit automaticWidthChanged(); |
549 | } |
550 | |
551 | QT_END_NAMESPACE |
552 | |