Revision control

Copy as Markdown

// 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
}
}
}