Revision control
Copy as Markdown
// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Tests that a leave and an enter reported in the same trigger callback are each
// processed with their own status.
//
// Teleporting the sphere from one volume into the other in a single step puts an
// eNOTIFY_TOUCH_LOST and an eNOTIFY_TOUCH_FOUND pair in the same batch. Either
// can come first, so each direction is a case of its own. Each volume lights up
// while it holds the sphere.
import QtCore
import QtQuick
import QtTest
import QtQuick3D
import QtQuick3D.Physics
import QtQuick3D.Physics.TestUtils
Item {
id: root
width: 640
height: 480
visible: true
PhysicsWorld {
id: world
// No gravity, so the sphere only moves when the test teleports it.
gravity: Qt.vector3d(0, 0, 0)
running: true
typicalLength: 1
typicalSpeed: 10
minimumTimestep: 16
maximumTimestep: 16
scene: viewport.scene
}
readonly property vector3d leftSeat: Qt.vector3d(-5, 0, 0)
readonly property vector3d rightSeat: Qt.vector3d(5, 0, 0)
View3D {
id: viewport
anchors.fill: parent
environment: SceneEnvironment {
clearColor: "#151a3f"
backgroundMode: SceneEnvironment.Color
}
PerspectiveCamera {
position: Qt.vector3d(0, 3, 18)
eulerRotation.x: -8
clipFar: 100
clipNear: 0.01
}
DirectionalLight {
eulerRotation.x: -40
eulerRotation.y: 30
}
component Volume: TriggerBody {
id: volume
property color tint: "#4488ff"
readonly property bool occupied: volume.collisionCount > 0
collisionShapes: BoxShape {
extents: Qt.vector3d(3, 3, 3)
}
Model {
source: "#Cube"
scale: Qt.vector3d(0.03, 0.03, 0.03)
materials: PrincipledMaterial {
baseColor: volume.tint
opacity: volume.occupied ? 0.55 : 0.15
lighting: PrincipledMaterial.NoLighting
}
}
}
Volume {
id: boxLeft
position: root.leftSeat
tint: "#4488ff"
// The sphere starts in here, so the first enter is on its own in the
// batch and only kicks the cycle off.
property int enters: 0
onBodyEntered: (body) => {
enters++;
if (enters === 1)
body.reset(root.rightSeat, Qt.vector3d(0, 0, 0));
else
settleLeft.restart();
}
}
Volume {
id: boxRight
position: root.rightSeat
tint: "#ff9a2e"
onBodyEntered: (body) => { settleRight.restart(); }
}
DynamicRigidBody {
id: sphere
position: root.leftSeat
sendTriggerReports: true
collisionShapes: SphereShape {
diameter: 1
}
Model {
source: "#Sphere"
scale: Qt.vector3d(0.01, 0.01, 0.01)
materials: PrincipledMaterial {
baseColor: "#e8e8e8"
roughness: 0.4
}
}
}
QtObject {
id: crossed
property bool toRight: false
property bool toLeft: false
}
// Judged after the arrival so the whole batch has been handled, which
// keeps the result independent of the order the pairs arrive in.
Timer {
id: settleRight
interval: 100
onTriggered: {
if (boxLeft.collisionCount === 0 && boxRight.collisionCount === 1)
crossed.toRight = true;
sphere.reset(root.leftSeat, Qt.vector3d(0, 0, 0));
}
}
Timer {
id: settleLeft
interval: 100
onTriggered: {
if (boxRight.collisionCount === 0 && boxLeft.collisionCount === 1)
crossed.toLeft = true;
sphere.reset(root.rightSeat, Qt.vector3d(0, 0, 0));
}
}
PhysicsTestCase {
name: "leave and enter in one batch, going right"
goalReached: crossed.toRight
}
PhysicsTestCase {
name: "leave and enter in one batch, going left"
goalReached: crossed.toLeft
}
}
}