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
10QT_BEGIN_NAMESPACE
11
12
13class QQuick3DXrItemPrivate : public QQuick3DNodePrivate
14{
15 Q_DECLARE_PUBLIC(QQuick3DXrItem)
16public:
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
70void 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
89void 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
124static 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
129void 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*/
189QQuick3DXrItem::QQuick3DXrItem(QQuick3DNode *parent)
190 : QQuick3DNode(*(new QQuick3DXrItemPrivate()), parent)
191{
192}
193
194QQuick3DXrItem::~QQuick3DXrItem()
195{
196 Q_D(QQuick3DXrItem);
197 if (d->m_XrView)
198 d->m_XrView->unregisterXrItem(xrItem: this);
199}
200
201void 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 */
231QQuickItem *QQuick3DXrItem::contentItem() const
232{
233 Q_D(const QQuick3DXrItem);
234 return d->m_contentItem;
235}
236
237void 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 */
258qreal QQuick3DXrItem::pixelsPerUnit() const
259{
260 Q_D(const QQuick3DXrItem);
261 return d->m_pixelsPerUnit;
262}
263
264void 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
289bool QQuick3DXrItem::manualPixelsPerUnit() const
290{
291 Q_D(const QQuick3DXrItem);
292 return d->m_manualPixelsPerUnit;
293}
294
295void 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
312qreal QQuick3DXrItem::width() const
313{
314 Q_D(const QQuick3DXrItem);
315 return d->m_width;
316}
317
318void 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
336qreal QQuick3DXrItem::height() const
337{
338 Q_D(const QQuick3DXrItem);
339 return d->m_height;
340}
341
342void 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
357bool 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
480QColor QQuick3DXrItem::color() const
481{
482 Q_D(const QQuick3DXrItem);
483 return d->m_color;
484}
485
486void 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
505bool QQuick3DXrItem::automaticHeight() const
506{
507 Q_D(const QQuick3DXrItem);
508 return d->m_automaticHeight;
509}
510
511void 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
533bool QQuick3DXrItem::automaticWidth() const
534{
535 Q_D(const QQuick3DXrItem);
536 return d->m_automaticWidth;
537}
538
539void 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
551QT_END_NAMESPACE
552

source code of qtquick3d/src/xr/quick3dxr/qquick3dxritem.cpp