Revision control

++ // Copyright (C) 2026 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++ // Tests what a TriggerBody reports as being inside it.
++ //
++ // bodyExited is documented as being emitted when the trigger is no longer
++ // penetrated by the body, and collisionCount as the number of bodies currently
++ // colliding with it. PhysX reports one pair per pair of shapes, so a body with
++ // more than one shape, or a trigger with more than one shape, is reported
++ // several times over, and the shapes of a body are all replaced whenever any one
++ // of them changes, so the pairs of the shapes that went away are reported as
++ // lost. Neither of the two documented promises holds unless the reports are
++ // followed per pair of shapes.
++ //
++ // One scene per thing that can happen to a body a trigger holds, left to right
++ // and front to back:
++ //
++ // two shapes, one leaves a body deleted while inside
++ // shapes rebuilt while in shapes destroyed while inside
++ // a trigger rebuilt a shape carried out a sleeping body
++ // a body that only listens a body that stops asking
++ //
++ // Both directions of reporting are per body: the trigger's bodyEntered and
++ // bodyExited, and the body's own enteredTriggerBody and exitedTriggerBody, which
++ // a body asks for separately and can ask for on its own.
++ //
++ // Bodies and shapes are deleted here while a trigger is holding them, and what
++ // it holds them by is pointers. Every handler below reads the body a report
++ // hands it and counts a body it did not expect as a hundred of them, so a stale
++ // pointer shows up as a failure rather than as a number that happens to look
++ // right. Worth running against a module built with -fsanitize=address, which
++ // makes a stale one that is read a use after free:
++ //
++ // ASAN_OPTIONS=detect_leaks=0 QML_IMPORT_PATH=<build>/qml \
++ // ./test_auto_trigger_counting
++ //
++ // The trigger volumes are drawn and light up while they report holding
++ // something, so the scene can also be judged by eye: every volume with a block
++ // in it has to be lit, and only those.
++
++ 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
++ // Nothing should move except what the test moves itself.
++ gravity: Qt.vector3d(0, 0, 0)
++ running: true
++ typicalLength: 100
++ typicalSpeed: 1000
++ minimumTimestep: 16
++ maximumTimestep: 16
++ scene: viewport.scene
++
++ // The scenes are stepped in simulated time, not wall clock time, so a
++ // renderer that cannot keep up makes the test slower instead of making
++ // it sample a scene that has not moved yet.
++ property int frames: 0
++ onFrameDone: world.frames++
++ }
++
++ View3D {
++ id: viewport
++ anchors.fill: parent
++
++ environment: SceneEnvironment {
++ clearColor: "#151a3f"
++ backgroundMode: SceneEnvironment.Color
++ }
++
++ PerspectiveCamera {
++ position: Qt.vector3d(500, 300, 2100)
++ eulerRotation.x: -9
++ clipFar: 6000
++ }
++
++ DirectionalLight {
++ eulerRotation.x: -35
++ eulerRotation.y: 25
++ }
++
++ // A trigger volume, drawn so it brightens while it reports holding a body.
++ component Volume: TriggerBody {
++ id: volume
++ property color tint: "#4488ff"
++ property real span: 300
++ readonly property bool occupied: volume.collisionCount > 0
++ // What the volume is drawn as holding, which is what it counts
++ // unless the scene it is in has a body it does not count.
++ property bool lit: volume.occupied
++
++ collisionShapes: BoxShape {
++ extents: Qt.vector3d(volume.span, volume.span, volume.span)
++ }
++
++ Model {
++ source: "#Cube"
++ scale: Qt.vector3d(volume.span / 100, volume.span / 100, volume.span / 100)
++ materials: PrincipledMaterial {
++ baseColor: volume.tint
++ opacity: volume.lit ? 0.55 : 0.14
++ lighting: PrincipledMaterial.NoLighting
++ }
++ }
++ }
++
++ // Reached once the simulation has taken "frames" steps since this was
++ // started, which is what every step of every scene below waits for.
++ component AfterFrames: Timer {
++ id: gate
++ property int frames: 36
++ property int startedAt: 0
++ signal reached()
++
++ interval: 16
++ repeat: true
++ onRunningChanged: if (running) gate.startedAt = world.frames
++ onTriggered: {
++ if (world.frames - gate.startedAt < gate.frames)
++ return;
++ gate.running = false;
++ gate.reached();
++ }
++ }
++
++ component Block: Model {
++ source: "#Cube"
++ scale: Qt.vector3d(0.4, 0.4, 0.4)
++ materials: PrincipledMaterial {
++ baseColor: "#e8e8e8"
++ roughness: 0.5
++ }
++ }
++
++ // ---- one body, two shapes, sliding out ----
++
++ Volume {
++ id: straddled
++ objectName: "straddled"
++ position: Qt.vector3d(-400, 0, 0)
++ tint: "#4488ff"
++ }
++
++ DynamicRigidBody {
++ id: strider
++ objectName: "strider"
++ isKinematic: true
++ sendTriggerReports: true
++ // Both directions of the same reports, which are both per body.
++ receiveTriggerReports: true
++ kinematicPosition: Qt.vector3d(-400 + slide.offset, 0, 0)
++ collisionShapes: [
++ BoxShape { extents: Qt.vector3d(40, 40, 40); position: Qt.vector3d(-100, 0, 0) },
++ BoxShape { extents: Qt.vector3d(40, 40, 40); position: Qt.vector3d(100, 0, 0) }
++ ]
++
++ Block { position: Qt.vector3d(-100, 0, 0) }
++ Block { position: Qt.vector3d(100, 0, 0) }
++ }
++
++ QtObject {
++ id: slide
++ property real offset: 0
++ property int exits: 0
++ // Read once the leading shape is outside the trigger and the trailing
++ // one is still 30 units inside it, and once both are clear.
++ property int countWhileHalfOut: -1
++ property int exitsWhileHalfOut: -1
++ property int countWhenOut: -1
++ property int exitsWhenOut: -1
++ // The same, as the body itself hears it.
++ property int enteredSelf: 0
++ property int exitedSelf: 0
++ property int selfEntersWhileHalfOut: -1
++ property int selfExitsWhileHalfOut: -1
++ property int selfExitsWhenOut: -1
++ }
++
++ Connections {
++ target: straddled
++ function onBodyExited(body) {
++ slide.exits += (body.objectName === "strider" ? 1 : 100);
++ }
++ }
++
++ Connections {
++ target: strider
++ function onEnteredTriggerBody(body) {
++ slide.enteredSelf += (body.objectName === "straddled" ? 1 : 100);
++ }
++ function onExitedTriggerBody(body) {
++ slide.exitedSelf += (body.objectName === "straddled" ? 1 : 100);
++ }
++ }
++
++ AfterFrames {
++ running: true
++ onReached: { slide.offset = 120; halfOutGate.running = true; }
++ }
++
++ AfterFrames {
++ id: halfOutGate
++ onReached: {
++ slide.countWhileHalfOut = straddled.collisionCount;
++ slide.exitsWhileHalfOut = slide.exits;
++ slide.selfEntersWhileHalfOut = slide.enteredSelf;
++ slide.selfExitsWhileHalfOut = slide.exitedSelf;
++ slide.offset = 600;
++ goneGate.running = true;
++ }
++ }
++
++ AfterFrames {
++ id: goneGate
++ onReached: {
++ slide.countWhenOut = straddled.collisionCount;
++ slide.exitsWhenOut = slide.exits;
++ slide.selfExitsWhenOut = slide.exitedSelf;
++ }
++ }
++
++ // ---- a body deleted while inside ----
++
++ Volume {
++ id: outlived
++ position: Qt.vector3d(1400, 0, 0)
++ tint: "#ff9a2e"
++ }
++
++ DynamicRigidBody {
++ id: doomed
++ objectName: "doomed"
++ sendTriggerReports: true
++ position: Qt.vector3d(1400, 0, 0)
++ collisionShapes: BoxShape {
++ extents: Qt.vector3d(40, 40, 40)
++ }
++
++ Block {}
++ }
++
++ QtObject {
++ id: deletion
++ property int countAfter: -1
++ }
++
++ AfterFrames {
++ frames: 56; running: true
++ onReached: { doomed.destroy(); deletedGate.running = true; }
++ }
++
++ AfterFrames {
++ id: deletedGate
++ // Long enough that the body's actor has actually been taken out of
++ // the scene, not just its QML object deleted.
++ frames: 75
++ onReached: deletion.countAfter = outlived.collisionCount
++ }
++
++ // ---- the shapes of a body inside rebuilt over and over ----
++
++ Volume {
++ id: shifting
++ position: Qt.vector3d(-400, 0, -900)
++ tint: "#63d471"
++ }
++
++ DynamicRigidBody {
++ id: wiggler
++ objectName: "wiggler"
++ isKinematic: true
++ sendTriggerReports: true
++ kinematicPosition: Qt.vector3d(-400 + wiggle.offset, 0, -900)
++ collisionShapes: BoxShape {
++ id: wigglingShape
++ extents: Qt.vector3d(40, 40, 40)
++ position: Qt.vector3d(wiggle.shapeX, 0, 0)
++ }
++
++ Block { position: wigglingShape.position }
++ }
++
++ QtObject {
++ id: wiggle
++ property real offset: 0
++ // Moving a shape within its body replaces every PhysX shape of that
++ // body, so this keeps them being rebuilt while the body sits inside.
++ property real shapeX: 0
++ property int enters: 0
++ property int exits: 0
++ property int countWhileRebuilt: -1
++ property int entersWhileRebuilt: -1
++ property int exitsWhileRebuilt: -1
++ property int countWhenOut: -1
++ property int exitsWhenOut: -1
++ }
++
++ Connections {
++ target: shifting
++ function onBodyEntered(body) {
++ wiggle.enters += (body.objectName === "wiggler" ? 1 : 100);
++ }
++ function onBodyExited(body) {
++ wiggle.exits += (body.objectName === "wiggler" ? 1 : 100);
++ }
++ }
++
++ Timer {
++ interval: 16; running: true; repeat: true
++ onTriggered: wiggle.shapeX = wiggle.shapeX > 0 ? -60 : 60
++ }
++
++ AfterFrames {
++ frames: 62; running: true
++ onReached: {
++ wiggle.countWhileRebuilt = shifting.collisionCount;
++ wiggle.entersWhileRebuilt = wiggle.enters;
++ wiggle.exitsWhileRebuilt = wiggle.exits;
++ wiggle.offset = 600;
++ wiggleGoneGate.running = true;
++ }
++ }
++
++ AfterFrames {
++ id: wiggleGoneGate
++ onReached: {
++ wiggle.countWhenOut = shifting.collisionCount;
++ wiggle.exitsWhenOut = wiggle.exits;
++ }
++ }
++
++ // ---- the shapes of a body inside destroyed one by one ----
++
++ Volume {
++ id: trimmed
++ position: Qt.vector3d(1400, 0, -900)
++ tint: "#ffd34e"
++ }
++
++ DynamicRigidBody {
++ id: shedder
++ objectName: "shedder"
++ isKinematic: true
++ sendTriggerReports: true
++ kinematicPosition: Qt.vector3d(1400, 0, -900)
++ collisionShapes: [
++ BoxShape {
++ id: keptShape
++ extents: Qt.vector3d(40, 40, 40)
++ position: Qt.vector3d(-60, 0, 0)
++ },
++ BoxShape {
++ id: droppedShape
++ extents: Qt.vector3d(40, 40, 40)
++ position: Qt.vector3d(60, 0, 0)
++ }
++ ]
++
++ Block { position: Qt.vector3d(-60, 0, 0) }
++ Block { position: Qt.vector3d(60, 0, 0) }
++ }
++
++ QtObject {
++ id: trim
++ property int exits: 0
++ property int countWithOneShape: -1
++ property int exitsWithOneShape: -1
++ property int countWithNoShapes: -1
++ property int exitsWithNoShapes: -1
++ }
++
++ Connections {
++ target: trimmed
++ function onBodyExited(body) {
++ trim.exits += (body.objectName === "shedder" ? 1 : 100);
++ }
++ }
++
++ AfterFrames {
++ frames: 44; running: true
++ onReached: { droppedShape.destroy(); oneShapeGate.running = true; }
++ }
++
++ AfterFrames {
++ id: oneShapeGate
++ frames: 44
++ onReached: {
++ trim.countWithOneShape = trimmed.collisionCount;
++ trim.exitsWithOneShape = trim.exits;
++ keptShape.destroy();
++ noShapesGate.running = true;
++ }
++ }
++
++ AfterFrames {
++ id: noShapesGate
++ frames: 44
++ onReached: {
++ trim.countWithNoShapes = trimmed.collisionCount;
++ trim.exitsWithNoShapes = trim.exits;
++ }
++ }
++
++ // ---- the shapes of the trigger itself rebuilt over and over ----
++
++ Volume {
++ id: resized
++ position: Qt.vector3d(-1600, 0, -1800)
++ tint: "#4dd0e1"
++ span: resize.span
++ }
++
++ DynamicRigidBody {
++ id: sitter
++ objectName: "sitter"
++ isKinematic: true
++ sendTriggerReports: true
++ kinematicPosition: Qt.vector3d(-1600, 0, -1800)
++ collisionShapes: BoxShape {
++ extents: Qt.vector3d(40, 40, 40)
++ }
++
++ Block {}
++ }
++
++ QtObject {
++ id: resize
++ property real span: 300
++ property int enters: 0
++ property int exits: 0
++ property int countWhileRebuilt: -1
++ property int entersWhileRebuilt: -1
++ property int exitsWhileRebuilt: -1
++ }
++
++ Connections {
++ target: resized
++ function onBodyEntered(body) {
++ resize.enters += (body.objectName === "sitter" ? 1 : 100);
++ }
++ function onBodyExited(body) {
++ resize.exits += (body.objectName === "sitter" ? 1 : 100);
++ }
++ }
++
++ Timer {
++ interval: 16; running: true; repeat: true
++ onTriggered: resize.span = resize.span > 320 ? 300 : 340
++ }
++
++ AfterFrames {
++ frames: 62; running: true
++ onReached: {
++ resize.countWhileRebuilt = resized.collisionCount;
++ resize.entersWhileRebuilt = resize.enters;
++ resize.exitsWhileRebuilt = resize.exits;
++ }
++ }
++
++ // ---- a shape carried out of the trigger by itself ----
++
++ Volume {
++ id: vacated
++ position: Qt.vector3d(500, 0, -1800)
++ tint: "#c86bff"
++ }
++
++ DynamicRigidBody {
++ id: stayer
++ objectName: "stayer"
++ isKinematic: true
++ sendTriggerReports: true
++ kinematicPosition: Qt.vector3d(500, 0, -1800)
++ // The body stays where it is and its only shape is moved out from
++ // under it, which the simulation reports as the pair of a shape that
++ // is already gone and nothing else.
++ collisionShapes: BoxShape {
++ id: carriedShape
++ extents: Qt.vector3d(40, 40, 40)
++ position: Qt.vector3d(carry.shapeX, 0, 0)
++ }
++
++ Block { position: carriedShape.position }
++ }
++
++ QtObject {
++ id: carry
++ property real shapeX: 0
++ property int exits: 0
++ property int countInside: -1
++ property int countOutside: -1
++ property int exitsOutside: -1
++ }
++
++ Connections {
++ target: vacated
++ function onBodyExited(body) {
++ carry.exits += (body.objectName === "stayer" ? 1 : 100);
++ }
++ }
++
++ AfterFrames {
++ running: true
++ onReached: {
++ carry.countInside = vacated.collisionCount;
++ carry.shapeX = 400;
++ carriedGate.running = true;
++ }
++ }
++
++ AfterFrames {
++ id: carriedGate
++ onReached: {
++ carry.countOutside = vacated.collisionCount;
++ carry.exitsOutside = carry.exits;
++ }
++ }
++
++ // ---- the shapes of a sleeping body inside rebuilt ----
++
++ Volume {
++ id: undisturbed
++ position: Qt.vector3d(2600, 0, -1800)
++ tint: "#ff7b7b"
++ }
++
++ DynamicRigidBody {
++ // Left to the simulation, and with nothing to move it, so it falls
++ // asleep where it is well before its shape is nudged.
++ id: dozer
++ objectName: "dozer"
++ sendTriggerReports: true
++ position: Qt.vector3d(2600, 0, -1800)
++ collisionShapes: BoxShape {
++ extents: Qt.vector3d(40, 40, 40)
++ position: Qt.vector3d(doze.shapeX, 0, 0)
++ }
++
++ Block { position: Qt.vector3d(doze.shapeX, 0, 0) }
++ }
++
++ QtObject {
++ id: doze
++ property real shapeX: 0
++ property int exits: 0
++ property int countAfterNudge: -1
++ property int exitsAfterNudge: -1
++ }
++
++ Connections {
++ target: undisturbed
++ function onBodyExited(body) {
++ doze.exits += (body.objectName === "dozer" ? 1 : 100);
++ }
++ }
++
++ AfterFrames {
++ // Well past the simulated second or so it takes the body to fall
++ // asleep where it is.
++ frames: 94; running: true
++ onReached: { doze.shapeX = 20; nudgedGate.running = true; }
++ }
++
++ AfterFrames {
++ id: nudgedGate
++ frames: 56
++ onReached: {
++ doze.countAfterNudge = undisturbed.collisionCount;
++ doze.exitsAfterNudge = doze.exits;
++ }
++ }
++
++ // ---- a body that only asked to be told itself ----
++
++ Volume {
++ id: unwitting
++ objectName: "unwitting"
++ position: Qt.vector3d(500, 0, -2700)
++ tint: "#9fe870"
++ // It does not count this body, so what it counts is not what it is
++ // drawn as holding.
++ lit: listen.enters > listen.exits
++ }
++
++ DynamicRigidBody {
++ id: listener
++ objectName: "listener"
++ isKinematic: true
++ // Without sendTriggerReports the trigger neither counts it nor
++ // reports it, but it is still told about itself, once for the two
++ // shapes it has inside.
++ receiveTriggerReports: true
++ kinematicPosition: Qt.vector3d(500 + listen.offset, 0, -2700)
++ collisionShapes: [
++ BoxShape { extents: Qt.vector3d(40, 40, 40); position: Qt.vector3d(-100, 0, 0) },
++ BoxShape { extents: Qt.vector3d(40, 40, 40); position: Qt.vector3d(100, 0, 0) }
++ ]
++
++ Block { position: Qt.vector3d(-100, 0, 0) }
++ Block { position: Qt.vector3d(100, 0, 0) }
++ }
++
++ QtObject {
++ id: listen
++ property real offset: 0
++ property int enters: 0
++ property int exits: 0
++ property int triggerReports: 0
++ property int entersInside: -1
++ property int countInside: -1
++ property int triggerReportsInside: -1
++ property int exitsWhenOut: -1
++ property int countWhenOut: -1
++ }
++
++ Connections {
++ target: listener
++ function onEnteredTriggerBody(body) {
++ listen.enters += (body.objectName === "unwitting" ? 1 : 100);
++ }
++ function onExitedTriggerBody(body) {
++ listen.exits += (body.objectName === "unwitting" ? 1 : 100);
++ }
++ }
++
++ Connections {
++ target: unwitting
++ // Neither of these is about a body that did not ask for them.
++ function onBodyEntered(body) { listen.triggerReports++; }
++ function onBodyExited(body) { listen.triggerReports++; }
++ }
++
++ AfterFrames {
++ running: true
++ onReached: {
++ listen.entersInside = listen.enters;
++ listen.countInside = unwitting.collisionCount;
++ listen.triggerReportsInside = listen.triggerReports;
++ listen.offset = 600;
++ listenGoneGate.running = true;
++ }
++ }
++
++ AfterFrames {
++ id: listenGoneGate
++ onReached: {
++ listen.exitsWhenOut = listen.exits;
++ listen.countWhenOut = unwitting.collisionCount;
++ }
++ }
++
++ // ---- a body that stops asking while it is inside ----
++
++ Volume {
++ id: forsaken
++ objectName: "forsaken"
++ position: Qt.vector3d(2600, 0, -2700)
++ tint: "#ff6fb5"
++ }
++
++ DynamicRigidBody {
++ id: quitter
++ objectName: "quitter"
++ isKinematic: true
++ // Turned off while the body is inside, so nothing is asked for by
++ // the time it leaves. The trigger still has to stop holding it, and
++ // still owes it the report it was holding it for.
++ sendTriggerReports: quit.asking
++ kinematicPosition: Qt.vector3d(2600 + quit.offset, 0, -2700)
++ collisionShapes: BoxShape {
++ id: quitterShape
++ extents: Qt.vector3d(40, 40, 40)
++ position: Qt.vector3d(quit.shapeX, 0, 0)
++ }
++
++ Block { position: quitterShape.position }
++ }
++
++ QtObject {
++ id: quit
++ property bool asking: true
++ property real offset: 0
++ // Moved once the body has stopped asking, to rebuild its shapes
++ // while it is inside: the pairs of the old shape have to be taken
++ // up again by the shape that replaces it, whatever the body is
++ // asking for by then.
++ property real shapeX: 0
++ property int exits: 0
++ property int countInside: -1
++ property int countAfterRebuild: -1
++ property int exitsAfterRebuild: -1
++ property int countWhenOut: -1
++ property int exitsWhenOut: -1
++ }
++
++ Connections {
++ target: forsaken
++ function onBodyExited(body) {
++ quit.exits += (body.objectName === "quitter" ? 1 : 100);
++ }
++ }
++
++ AfterFrames {
++ running: true
++ onReached: {
++ quit.countInside = forsaken.collisionCount;
++ quit.asking = false;
++ quit.shapeX = 20;
++ quitRebuiltGate.running = true;
++ }
++ }
++
++ AfterFrames {
++ id: quitRebuiltGate
++ onReached: {
++ quit.countAfterRebuild = forsaken.collisionCount;
++ quit.exitsAfterRebuild = quit.exits;
++ quit.offset = 600;
++ quitGoneGate.running = true;
++ }
++ }
++
++ AfterFrames {
++ id: quitGoneGate
++ onReached: {
++ quit.countWhenOut = forsaken.collisionCount;
++ quit.exitsWhenOut = quit.exits;
++ }
++ }
++
++ // ---- a body relaying its overlap from one shape to another ----
++
++ Volume {
++ id: relayed
++ objectName: "relayed"
++ position: Qt.vector3d(-1600, 0, -2700)
++ span: 100
++ tint: "#7de08d"
++ }
++
++ DynamicRigidBody {
++ id: relayer
++ objectName: "relayer"
++ isKinematic: true
++ sendTriggerReports: true
++ // Two shapes, far enough apart that only one of them is ever inside
++ // the volume. Moved by the distance between them in one step, which
++ // takes the one that was inside out and brings the other one in, so
++ // PhysX reports the pair it lost and the pair it found in the same
++ // batch. The body has not left, and must not be reported as having.
++ kinematicPosition: Qt.vector3d(-1600 + relay.offset, 0, -2700)
++ collisionShapes: [
++ BoxShape {
++ id: relayerLeft
++ extents: Qt.vector3d(40, 40, 40)
++ position: Qt.vector3d(-200, 0, 0)
++ },
++ BoxShape {
++ id: relayerRight
++ extents: Qt.vector3d(40, 40, 40)
++ position: Qt.vector3d(200, 0, 0)
++ }
++ ]
++
++ Block { position: relayerLeft.position }
++ Block { position: relayerRight.position }
++ }
++
++ QtObject {
++ id: relay
++ // The left shape starts inside, the right one outside.
++ property real offset: 200
++ property int enters: 0
++ property int exits: 0
++ property int countInside: -1
++ property int entersInside: -1
++ property int countAfterRelay: -1
++ property int entersAfterRelay: -1
++ property int exitsAfterRelay: -1
++ }
++
++ Connections {
++ target: relayed
++ function onBodyEntered(body) {
++ relay.enters += (body.objectName === "relayer" ? 1 : 100);
++ }
++ function onBodyExited(body) {
++ relay.exits += (body.objectName === "relayer" ? 1 : 100);
++ }
++ }
++
++ AfterFrames {
++ running: true
++ onReached: {
++ relay.countInside = relayed.collisionCount;
++ relay.entersInside = relay.enters;
++ relay.offset = -200;
++ relayGate.running = true;
++ }
++ }
++
++ AfterFrames {
++ id: relayGate
++ onReached: {
++ relay.countAfterRelay = relayed.collisionCount;
++ relay.entersAfterRelay = relay.enters;
++ relay.exitsAfterRelay = relay.exits;
++ }
++ }
++
++ // ---- a body that starts asking while it is already inside ----
++
++ Volume {
++ id: unasked
++ objectName: "unasked"
++ position: Qt.vector3d(1400, 0, -2700)
++ tint: "#c39bd3"
++ }
++
++ DynamicRigidBody {
++ id: silent
++ objectName: "silent"
++ isKinematic: true
++ // Asks for nothing as it enters, and starts asking once it is
++ // inside. What it asks for then decides nothing about this stay, so
++ // it must not be told it entered something it never crossed into.
++ sendTriggerReports: mute.asking
++ receiveTriggerReports: mute.asking
++ kinematicPosition: Qt.vector3d(1400, 0, -2700)
++ collisionShapes: BoxShape {
++ id: silentShape
++ extents: Qt.vector3d(40, 40, 40)
++ position: Qt.vector3d(mute.shapeX, 0, 0)
++ }
++
++ Block { position: silentShape.position }
++ }
++
++ QtObject {
++ id: mute
++ property bool asking: false
++ // Rebuilds the body's shapes while it sits inside, which is what
++ // has PhysX report its pairs as found again.
++ property real shapeX: 0
++ property int enters: 0
++ property int selfEnters: 0
++ property int countAfterAsking: -1
++ property int entersAfterAsking: -1
++ property int selfEntersAfterAsking: -1
++ }
++
++ Timer {
++ interval: 16; running: true; repeat: true
++ onTriggered: mute.shapeX = mute.shapeX > 0 ? -20 : 20
++ }
++
++ Connections {
++ target: unasked
++ function onBodyEntered(body) {
++ mute.enters += (body.objectName === "silent" ? 1 : 100);
++ }
++ }
++
++ Connections {
++ target: silent
++ function onEnteredTriggerBody(body) {
++ mute.selfEnters += (body.objectName === "unasked" ? 1 : 100);
++ }
++ }
++
++ AfterFrames {
++ running: true
++ onReached: {
++ mute.asking = true;
++ mutedGate.running = true;
++ }
++ }
++
++ AfterFrames {
++ id: mutedGate
++ onReached: {
++ mute.countAfterAsking = unasked.collisionCount;
++ mute.entersAfterAsking = mute.enters;
++ mute.selfEntersAfterAsking = mute.selfEnters;
++ }
++ }
++
++ // ---- what all of that has to add up to ----
++
++ PhysicsTestCase {
++ name: "one shape of two leaving does not report the body as gone"
++ goalReached: slide.countWhileHalfOut === 1 && slide.exitsWhileHalfOut === 0
++ }
++
++ PhysicsTestCase {
++ name: "the last shape leaving does report the body as gone"
++ goalReached: slide.countWhenOut === 0 && slide.exitsWhenOut === 1
++ }
++
++ PhysicsTestCase {
++ name: "the body hears about entering once, not once per shape"
++ goalReached: slide.selfEntersWhileHalfOut === 1 && slide.selfExitsWhileHalfOut === 0
++ }
++
++ PhysicsTestCase {
++ name: "the body hears about leaving when its last shape leaves"
++ goalReached: slide.selfExitsWhenOut === 1
++ }
++
++ PhysicsTestCase {
++ name: "a body deleted while inside stops being counted"
++ goalReached: deletion.countAfter === 0
++ }
++
++ PhysicsTestCase {
++ name: "a body whose shapes are rebuilt stays inside, and only once"
++ goalReached: wiggle.countWhileRebuilt === 1 && wiggle.entersWhileRebuilt === 1
++ && wiggle.exitsWhileRebuilt === 0
++ }
++
++ PhysicsTestCase {
++ name: "a body whose shapes were rebuilt still reports leaving"
++ goalReached: wiggle.countWhenOut === 0 && wiggle.exitsWhenOut === 1
++ }
++
++ PhysicsTestCase {
++ name: "one shape of two destroyed does not report the body as gone"
++ goalReached: trim.countWithOneShape === 1 && trim.exitsWithOneShape === 0
++ }
++
++ PhysicsTestCase {
++ name: "the last shape destroyed does report the body as gone"
++ goalReached: trim.countWithNoShapes === 0 && trim.exitsWithNoShapes === 1
++ }
++
++ PhysicsTestCase {
++ name: "rebuilding the shapes of the trigger reports nothing"
++ goalReached: resize.countWhileRebuilt === 1 && resize.entersWhileRebuilt === 1
++ && resize.exitsWhileRebuilt === 0
++ }
++
++ PhysicsTestCase {
++ name: "a shape moved out reports the body as gone"
++ goalReached: carry.countInside === 1 && carry.countOutside === 0
++ && carry.exitsOutside === 1
++ }
++
++ PhysicsTestCase {
++ name: "nudging the shape of a sleeping body keeps it inside"
++ goalReached: doze.countAfterNudge === 1 && doze.exitsAfterNudge === 0
++ }
++
++ PhysicsTestCase {
++ name: "a body that only listens is told once and counted not at all"
++ goalReached: listen.entersInside === 1 && listen.countInside === 0
++ && listen.triggerReportsInside === 0
++ }
++
++ PhysicsTestCase {
++ name: "a body that only listens is told about leaving once"
++ goalReached: listen.exitsWhenOut === 1 && listen.countWhenOut === 0
++ }
++
++ PhysicsTestCase {
++ name: "a body that stops asking while inside is still held"
++ goalReached: quit.countInside === 1 && quit.countAfterRebuild === 1
++ && quit.exitsAfterRebuild === 0
++ }
++
++ PhysicsTestCase {
++ name: "a body that starts asking while inside is not told it entered"
++ goalReached: mute.countAfterAsking === 0 && mute.entersAfterAsking === 0
++ && mute.selfEntersAfterAsking === 0
++ }
++
++ PhysicsTestCase {
++ name: "a body relaying its overlap in one step is still inside"
++ goalReached: relay.countInside === 1 && relay.entersInside === 1
++ && relay.countAfterRelay === 1
++ }
++
++ PhysicsTestCase {
++ name: "a body relaying its overlap in one step is not reported again"
++ goalReached: relay.countAfterRelay === 1 && relay.entersAfterRelay === 1
++ && relay.exitsAfterRelay === 0
++ }
++
++ PhysicsTestCase {
++ name: "a body that stops asking while inside is still let go of"
++ goalReached: quit.countWhenOut === 0 && quit.exitsWhenOut === 1
++ }
++ }
++ }