Revision control
+ // Copyright (C) 2026 The Qt Company Ltd.
+ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+ // Walks the world's bodies while one of them has been deleted. A deleted node
+ // leaves its backend behind until a frame takes it out, so the world is stopped
+ // first, since nothing else clears it, and then each of the three things that
+ // walk every body is asked for: a new default density, debug drawing off, and a
+ // move to another scene. Each of them crashed.
+ //
+ // Reaching the end is the assertion. The walks are named across the top and lit
+ // as they are survived, so the scene can be judged by eye too.
+
+ import QtQuick
+ import QtTest
+ import QtQuick3D
+ import QtQuick3D.Physics
+ import QtQuick3D.Physics.TestUtils
+
+ Item {
+ width: 640
+ height: 480
+ visible: true
+
+ PhysicsWorld {
+ id: world
+ gravity: Qt.vector3d(0, -981, 0)
+ running: true
+ forceDebugDraw: true
+ typicalLength: 100
+ typicalSpeed: 1000
+ minimumTimestep: 15
+ maximumTimestep: 15
+ scene: viewport.scene
+ }
+
+ QtObject {
+ id: walked
+ property bool density: false
+ property bool debugDraw: false
+ property bool scene: false
+ }
+
+ // A step apart, so the delete the first one asks for, which is deferred,
+ // has happened by the time the next one walks the bodies.
+ Timer {
+ interval: 300
+ repeat: true
+ running: true
+ property int step: 0
+ onTriggered: {
+ switch (++step) {
+ case 1:
+ // Stopped, so no frame takes the removed backend out from
+ // under the walks below.
+ world.running = false;
+ boxes.model = 2;
+ break;
+ case 2:
+ world.defaultDensity = 0.002;
+ walked.density = true;
+ break;
+ case 3:
+ world.forceDebugDraw = false;
+ walked.debugDraw = true;
+ break;
+ case 4:
+ world.scene = elsewhere;
+ walked.scene = true;
+ break;
+ }
+ }
+ }
+
+ View3D {
+ id: viewport
+ anchors.fill: parent
+
+ environment: SceneEnvironment {
+ clearColor: "#151a3f"
+ backgroundMode: SceneEnvironment.Color
+ }
+
+ PerspectiveCamera {
+ position: Qt.vector3d(0, 100, 600)
+ clipFar: 4000
+ }
+
+ DirectionalLight {
+ eulerRotation: Qt.vector3d(-35, 25, 0)
+ brightness: 1.5
+ ambientColor: "#3a3a44"
+ }
+
+ // What the world is moved to, so that moving it means every body
+ // leaves the scene it is in.
+ Node {
+ id: elsewhere
+ }
+
+ StaticRigidBody {
+ position: Qt.vector3d(0, -150, 0)
+ collisionShapes: BoxShape {
+ extents: Qt.vector3d(800, 40, 400)
+ }
+ Model {
+ source: "#Cube"
+ scale: Qt.vector3d(8, 0.4, 4)
+ materials: PrincipledMaterial {
+ baseColor: "#3f8f4f"
+ roughness: 0.85
+ }
+ }
+ }
+
+ // The last of these is the one that gets deleted.
+ Repeater3D {
+ id: boxes
+ model: 3
+ DynamicRigidBody {
+ required property int index
+ position: Qt.vector3d(index * 120 - 120, 200, 0)
+ collisionShapes: BoxShape {
+ extents: Qt.vector3d(60, 60, 60)
+ }
+ Model {
+ source: "#Cube"
+ scale: Qt.vector3d(0.6, 0.6, 0.6)
+ materials: PrincipledMaterial {
+ baseColor: index === 2 ? "#c0504d" : "#4a86e8"
+ roughness: 0.5
+ }
+ }
+ }
+ }
+ }
+
+ Row {
+ anchors.top: parent.top
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.topMargin: 12
+ spacing: 24
+
+ component Marker: Text {
+ required property bool done
+ color: done ? "#7ee08a" : "#9aa0b4"
+ font.pixelSize: 16
+ }
+
+ Marker { text: "density"; done: walked.density }
+ Marker { text: "debug draw"; done: walked.debugDraw }
+ Marker { text: "scene"; done: walked.scene }
+ }
+
+ PhysicsTestCase {
+ name: "giving every body a new default density"
+ goalReached: walked.density
+ }
+
+ PhysicsTestCase {
+ name: "taking every body's debug drawing away"
+ goalReached: walked.debugDraw
+ }
+
+ PhysicsTestCase {
+ name: "moving every body to another scene"
+ goalReached: walked.scene
+ }
+ }