Revision control

++ // Copyright (C) 2022 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++ import QtCore
++ import QtTest
++ import QtQuick3D
++ import QtQuick3D.Physics
++ import QtQuick
++ import QtQuick3D.Physics.TestUtils
++
++ Item {
++ width: 640
++ height: 480
++ visible: true
++
++ PhysicsWorld {
++ gravity: Qt.vector3d(0, -9.81, 0)
++ running: true
++ forceDebugDraw: true
++ typicalLength: 1
++ typicalSpeed: 10
++ minimumTimestep: 15
++ maximumTimestep: 15
++ scene: viewport.scene
++ }
++ property bool simulationActuallyRunning: false
++
++ View3D {
++ id: viewport
++ anchors.fill: parent
++
++ environment: SceneEnvironment {
++ clearColor: "#151a3f"
++ backgroundMode: SceneEnvironment.Color
++ }
++
++ Node {
++ id: scene
++ PerspectiveCamera {
++ id: camera1
++ position: Qt.vector3d(0, 5, 10)
++ eulerRotation: Qt.vector3d(-20, 0, 0)
++ clipFar: 50
++ clipNear: 0.01
++ }
++
++ DirectionalLight {
++ eulerRotation.x: -45
++ eulerRotation.y: 45
++ }
++
++ Node {
++ id: fallingBoxNode
++ StaticRigidBody {
++ position: Qt.vector3d(0, -1, 0)
++ collisionShapes: BoxShape {
++ extents: Qt.vector3d(2,1,2)
++ }
++ Model {
++ source: "#Cube"
++ scale: Qt.vector3d(2, 1, 2).times(0.01)
++ eulerRotation: Qt.vector3d(0, 90, 0)
++ materials: PrincipledMaterial {
++ baseColor: "green"
++ }
++ }
++ }
++
++ TestCube {
++ id: fallingBox
++ y: 1
++ color: "yellow"
++ }
++ }
++
++ Node {
++ id: triggerNode
++ x: 5
++
++ TriggerBody {
++ id: triggerBox
++ position: Qt.vector3d(0, 3, 0)
++ collisionShapes: BoxShape {
++ extents: Qt.vector3d(1, 2, 1)
++ }
++
++ onBodyEntered: (body) => {
++ if (body.hasOwnProperty('inArea'))
++ body.inArea = true;
++ }
++ onBodyExited: (body) => {
++ if (body.hasOwnProperty('inArea'))
++ body.inArea = false;
++ }
++ }
++
++ DynamicRigidBody {
++ id: collisionSphere
++ massMode: DynamicRigidBody.CustomDensity
++ density: 1000
++ position: Qt.vector3d(0, 6, 0)
++ sendTriggerReports: true
++ property bool inArea: false
++
++ collisionShapes: SphereShape {
++ id: sphereShape
++ diameter: 1
++ }
++ Model {
++ source: "#Sphere"
++ materials: PrincipledMaterial {
++ baseColor: collisionSphere.inArea ? "yellow" : "red"
++ }
++ scale: Qt.vector3d(0.01, 0.01, 0.01)
++ }
++ }
++ }
++
++ Node {
++ id: dynamicCreationNode
++ x: -5
++ StaticRigidBody {
++ position: Qt.vector3d(0, -1, 0)
++ collisionShapes: BoxShape {
++ extents: Qt.vector3d(2,1,2)
++ }
++ Model {
++ source: "#Cube"
++ scale: Qt.vector3d(2, 1, 2).times(0.01)
++ eulerRotation: Qt.vector3d(0, 90, 0)
++ materials: PrincipledMaterial {
++ baseColor: "red"
++ }
++ }
++ }
++
++ property int frame: 0;
++ property var createdObject: null
++ property bool createdObjectIsStable: createdObject ? createdObject.stable : false
++ function checkStable() {
++ if (createdObject && frame > 10)
++ createdObject.checkStable()
++ frame += 1
++ }
++ Component {
++ id: spawnComponent
++ TestCube {
++ color: "cyan"
++ y: 1
++ }
++ }
++
++ PhysicsTestCase {
++ name: "DynamicCreation"
++ goalReached: dynamicCreationNode.createdObjectIsStable
++ function test_created_object_is_simulated() {
++ fuzzyCompare(dynamicCreationNode.createdObject.y, 0, 0.001)
++ }
++ }
++ }
++
++ Node {
++ id: frictionNode
++ z: -10
++ eulerRotation.z: 20
++ PhysicsMaterial {
++ id: frictionMaterial
++ dynamicFriction: 0
++ staticFriction: 0
++ }
++ StaticRigidBody {
++ position: Qt.vector3d(0, -1, 0)
++ collisionShapes: BoxShape {
++ id: slide
++ extents: Qt.vector3d(10, 1, 2)
++ }
++ physicsMaterial: frictionMaterial
++
++ Model {
++ source: "#Cube"
++ scale: slide.extents.times(0.01)
++ materials: PrincipledMaterial {
++ baseColor: "orange"
++ }
++ }
++ }
++ TestCube {
++ id: slidingBox
++ x: 4
++ color: "red"
++ physicsMaterial: frictionMaterial
++ property real brakePoint: -9999
++ }
++ PhysicsTestCase {
++ name: "Friction"
++ goalReached: timer.simulationSteps > 20
++ function test_low_friction() {
++ verify(!slidingBox.stable)
++ verify(slidingBox.x < 4)
++ }
++ function test_set_high_friction() {
++ frictionMaterial.dynamicFriction = 0.5
++ frictionMaterial.staticFriction = 0.5
++ slidingBox.brakePoint = slidingBox.x
++ }
++ }
++ PhysicsTestCase {
++ name: "Friction2"
++ goalReached: slidingBox.stable
++ function test_high_friction() {
++ fuzzyCompare(slidingBox.y, 0, 0.001)
++ verify(slidingBox.x < slidingBox.brakePoint)
++ }
++ }
++ }
++
++ Node {
++ id: heightFieldNode
++ z: -5
++ x: -5
++ StaticRigidBody {
++ position: "0, 0, 0"
++ collisionShapes: HeightFieldShape {
++ source: "qrc:/data/hf.png"
++ extents: "8, 2, 4"
++ }
++ physicsMaterial: frictionMaterial
++ }
++
++ DynamicRigidBody {
++ id: hfBall
++ massMode: DynamicRigidBody.CustomDensity
++ density: 1000
++ scale: "0.5, 0.5, 0.5"
++ position: Qt.vector3d(-3, 1, 0)
++
++ collisionShapes: SphereShape {
++ diameter: 1
++ }
++ Model {
++ source: "#Sphere"
++ materials: PrincipledMaterial {
++ baseColor: "yellow"
++ }
++ scale: Qt.vector3d(0.01, 0.01, 0.01)
++ }
++ property bool stable: false
++ property vector3d prevPos: "9999,9999,9999"
++ property vector3d prevPos2: "9999,9999,9999"
++ function fuzzyEquals(a, b) {
++ return Math.abs(a.x - b.x) < 0.01 && Math.abs(a.y - b.y) < 0.001 && Math.abs(a.z - b.z) < 0.01
++ }
++ function checkStable() {
++ // Compare against the previous two positions to avoid triggering on the top of a bounce
++ if (fuzzyEquals(position, prevPos) && fuzzyEquals(position, prevPos2)) {
++ stable = true
++ } else {
++ prevPos2 = prevPos
++ prevPos = position
++ }
++ }
++ }
++
++ PhysicsTestCase {
++ name: "HeightField"
++ goalReached: hfBall.stable
++ function test_ball_position() {
++ fuzzyCompare(hfBall.x, 2, 0.5)
++ fuzzyCompare(hfBall.y, -0.75, 0.1)
++ fuzzyCompare(hfBall.z, 0, 0.5)
++ }
++ }
++ }
++
++
++ // Trick to get a callback when the physics simulation advances, no matter how slowly the CI machine is running
++ DynamicRigidBody {
++ id: timer
++ position: "-10, 100, -10"
++ collisionShapes: SphereShape {
++ diameter: 2
++ }
++ property int simulationSteps: 0
++ onPositionChanged: {
++ // Only do the check(s) every other step, since we have no guarantees on signal emission order
++ if (simulationSteps % 2) {
++ fallingBox.checkStable()
++ dynamicCreationNode.checkStable()
++ slidingBox.checkStable()
++ hfBall.checkStable()
++ simulationActuallyRunning = true
++ }
++ simulationSteps++
++ }
++ }
++
++ DynamicRigidBody {
++ id: velocityBody
++ position: Qt.vector3d(1000, 1000, 1000)
++ gravityEnabled: false
++ collisionShapes: SphereShape {
++ diameter: 0.1
++ }
++ Component.onCompleted: {
++ setLinearVelocity(Qt.vector3d(100, 0, 0))
++ setAngularVelocity(Qt.vector3d(0, 1, 0))
++ }
++
++ // The velocities as they were when the body started to report sleeping,
++ // which it only does once its simulation is disabled.
++ property vector3d linearVelocityWhenAsleep: Qt.vector3d(-1, -1, -1)
++ property vector3d angularVelocityWhenAsleep: Qt.vector3d(-1, -1, -1)
++ onIsSleepingChanged: {
++ if (velocityBody.isSleeping) {
++ velocityBody.linearVelocityWhenAsleep = velocityBody.linearVelocity
++ velocityBody.angularVelocityWhenAsleep = velocityBody.angularVelocity
++ }
++ }
++ }
++ }
++ }
++
++ PhysicsTestCase {
++ name: "SceneStart"
++ goalReached: simulationActuallyRunning
++ function test_collision_sphere() {
++ verify(!collisionSphere.inArea)
++ compare(triggerEnterSpy.count, 0)
++ compare(triggerExitSpy.count, 0)
++ }
++ function test_falling_box() {
++ verify(fallingBox.y > 0)
++ compare(fallingBox.isSleeping, false)
++ }
++ function test_spawn() {
++ var obj = spawnComponent.createObject(dynamicCreationNode)
++ verify(obj.y === 1)
++ dynamicCreationNode.createdObject = obj
++ }
++ function test_velocity_properties() {
++ fuzzyCompare(velocityBody.linearVelocity.x, 100, 0.1)
++ fuzzyCompare(velocityBody.linearVelocity.y, 0, 0.001)
++ fuzzyCompare(velocityBody.linearVelocity.z, 0, 0.001)
++ fuzzyCompare(velocityBody.angularVelocity.x, 0, 0.001)
++ verify(velocityBody.angularVelocity.y > 0)
++ fuzzyCompare(velocityBody.angularVelocity.z, 0, 0.001)
++ verify(linearVelocitySpy.count > 0)
++ verify(angularVelocitySpy.count > 0)
++ }
++ function test_velocity_when_disabled() {
++ velocityBody.simulationEnabled = false
++ tryCompare(velocityBody, "isSleeping", true)
++ compare(velocityBody.linearVelocityWhenAsleep, Qt.vector3d(0, 0, 0))
++ compare(velocityBody.angularVelocityWhenAsleep, Qt.vector3d(0, 0, 0))
++ }
++
++ SignalSpy {
++ id: linearVelocitySpy
++ target: velocityBody
++ signalName: "linearVelocityChanged"
++ }
++
++ SignalSpy {
++ id: angularVelocitySpy
++ target: velocityBody
++ signalName: "angularVelocityChanged"
++ }
++ }
++
++ PhysicsTestCase {
++ name: "FallingBox"
++ goalReached: fallingBox.stable
++ function test_stable_position() {
++ fuzzyCompare(fallingBox.y, 0, 0.0001)
++ fuzzyCompare(fallingBox.eulerRotation.x, 0, 1)
++ fuzzyCompare(fallingBox.eulerRotation.z, 0, 1)
++ compare(fallingBox.isSleeping, true)
++ }
++ }
++
++ PhysicsTestCase {
++ name: "TriggerBody"
++ goalReached: collisionSphere.y < 0
++ function test_enter_exit() {
++ compare(triggerEnterSpy.count, 1)
++ compare(triggerExitSpy.count, 1)
++ }
++ SignalSpy {
++ id: triggerEnterSpy
++ target: triggerBox
++ signalName: "onBodyEntered"
++ }
++ SignalSpy {
++ id: triggerExitSpy
++ target: triggerBox
++ signalName: "onBodyExited"
++ }
++ }
++ }