dacha 0.18.0-alpha.8 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/contrib/components/index.d.ts +1 -0
- package/build/contrib/components/index.js +1 -0
- package/build/contrib/components/interpolation/index.d.ts +85 -0
- package/build/contrib/components/interpolation/index.js +101 -0
- package/build/contrib/components/rigid-body/index.d.ts +108 -14
- package/build/contrib/components/rigid-body/index.js +191 -22
- package/build/contrib/systems/animator/index.d.ts +3 -2
- package/build/contrib/systems/animator/index.js +6 -3
- package/build/contrib/systems/behavior-system/system.d.ts +4 -2
- package/build/contrib/systems/behavior-system/system.js +11 -3
- package/build/contrib/systems/behavior-system/types.d.ts +7 -10
- package/build/contrib/systems/character-controller/index.d.ts +4 -2
- package/build/contrib/systems/character-controller/index.js +22 -17
- package/build/contrib/systems/character-controller/one-way-validator.d.ts +15 -0
- package/build/contrib/systems/character-controller/one-way-validator.js +73 -0
- package/build/contrib/systems/game-stats-meter/index.d.ts +4 -3
- package/build/contrib/systems/game-stats-meter/index.js +11 -10
- package/build/contrib/systems/index.d.ts +2 -0
- package/build/contrib/systems/index.js +1 -0
- package/build/contrib/systems/interpolator/api.d.ts +40 -0
- package/build/contrib/systems/interpolator/api.js +70 -0
- package/build/contrib/systems/interpolator/index.d.ts +3 -0
- package/build/contrib/systems/interpolator/index.js +2 -0
- package/build/contrib/systems/interpolator/system.d.ts +31 -0
- package/build/contrib/systems/interpolator/system.js +96 -0
- package/build/contrib/systems/interpolator/tests/helpers.d.ts +13 -0
- package/build/contrib/systems/interpolator/tests/helpers.js +45 -0
- package/build/contrib/systems/interpolator/utils.d.ts +5 -0
- package/build/contrib/systems/interpolator/utils.js +41 -0
- package/build/contrib/systems/physics-system/api.d.ts +45 -1
- package/build/contrib/systems/physics-system/api.js +49 -0
- package/build/contrib/systems/physics-system/consts.d.ts +13 -0
- package/build/contrib/systems/physics-system/consts.js +17 -0
- package/build/contrib/systems/physics-system/physics-system.d.ts +2 -2
- package/build/contrib/systems/physics-system/physics-system.js +24 -4
- package/build/contrib/systems/physics-system/subsystems/collision-detection/dynamic-aabb-tree/index.d.ts +1 -1
- package/build/contrib/systems/physics-system/subsystems/collision-detection/dynamic-aabb-tree/index.js +21 -15
- package/build/contrib/systems/physics-system/subsystems/collision-detection/index.d.ts +11 -11
- package/build/contrib/systems/physics-system/subsystems/collision-detection/index.js +133 -125
- package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/box-box/check-boxes-intersection.js +5 -3
- package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/box-box/utils.d.ts +1 -0
- package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/box-box/utils.js +2 -1
- package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/box-capsule/check-box-and-capsule-intersection.js +4 -1
- package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/box-segment/check-box-and-segment-intersection.js +4 -2
- package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/common/points.d.ts +2 -0
- package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/common/points.js +16 -0
- package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/index.js +2 -0
- package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/tests/helpers.js +3 -14
- package/build/contrib/systems/physics-system/subsystems/collision-detection/query-utils.d.ts +5 -0
- package/build/contrib/systems/physics-system/subsystems/collision-detection/query-utils.js +97 -81
- package/build/contrib/systems/physics-system/subsystems/collision-detection/shape-cast-checkers/tests/helpers.js +2 -7
- package/build/contrib/systems/physics-system/subsystems/collision-detection/tests/assertions.d.ts +1 -0
- package/build/contrib/systems/physics-system/subsystems/collision-detection/tests/assertions.js +7 -0
- package/build/contrib/systems/physics-system/subsystems/collision-detection/types.d.ts +0 -15
- package/build/contrib/systems/physics-system/subsystems/collision-detection/utils.d.ts +3 -0
- package/build/contrib/systems/physics-system/subsystems/collision-detection/utils.js +9 -0
- package/build/contrib/systems/physics-system/subsystems/constraint-solver/contact-state-manager.d.ts +63 -0
- package/build/contrib/systems/physics-system/subsystems/constraint-solver/contact-state-manager.js +170 -0
- package/build/contrib/systems/physics-system/subsystems/constraint-solver/contact-utils.d.ts +5 -0
- package/build/contrib/systems/physics-system/subsystems/constraint-solver/contact-utils.js +41 -0
- package/build/contrib/systems/physics-system/subsystems/constraint-solver/impulse-utils.d.ts +16 -0
- package/build/contrib/systems/physics-system/subsystems/constraint-solver/impulse-utils.js +77 -0
- package/build/contrib/systems/physics-system/subsystems/constraint-solver/index.d.ts +53 -4
- package/build/contrib/systems/physics-system/subsystems/constraint-solver/index.js +334 -116
- package/build/contrib/systems/physics-system/subsystems/constraint-solver/one-way-validator.d.ts +11 -0
- package/build/contrib/{utils → systems/physics-system/subsystems/constraint-solver}/one-way-validator.js +22 -25
- package/build/contrib/systems/physics-system/subsystems/constraint-solver/sleep-support-tracker.d.ts +13 -0
- package/build/contrib/systems/physics-system/subsystems/constraint-solver/sleep-support-tracker.js +42 -0
- package/build/contrib/systems/physics-system/subsystems/constraint-solver/tests/helpers.d.ts +4 -0
- package/build/contrib/systems/physics-system/subsystems/constraint-solver/tests/helpers.js +20 -0
- package/build/contrib/systems/physics-system/subsystems/physics/index.d.ts +15 -4
- package/build/contrib/systems/physics-system/subsystems/physics/index.js +131 -53
- package/build/contrib/systems/physics-system/subsystems/physics/mass-properties.d.ts +2 -0
- package/build/contrib/systems/physics-system/subsystems/physics/mass-properties.js +55 -0
- package/build/contrib/systems/physics-system/tests/helpers.d.ts +5 -2
- package/build/contrib/systems/physics-system/tests/helpers.js +11 -2
- package/build/contrib/systems/physics-system/types.d.ts +18 -2
- package/build/contrib/systems/renderer/actor-render-tree.js +15 -6
- package/build/contrib/systems/renderer/filters/index.d.ts +1 -1
- package/build/contrib/systems/renderer/filters/index.js +1 -1
- package/build/contrib/systems/renderer/material/index.d.ts +2 -1
- package/build/contrib/systems/renderer/material/index.js +1 -1
- package/build/contrib/systems/renderer/renderer.d.ts +2 -2
- package/build/contrib/systems/renderer/renderer.js +3 -4
- package/build/contrib/systems/renderer/types.d.ts +0 -3
- package/build/engine/data-lib/index.d.ts +1 -1
- package/build/engine/data-lib/index.js +1 -1
- package/build/engine/data-lib/pool.d.ts +9 -0
- package/build/engine/data-lib/pool.js +19 -0
- package/build/engine/engine.js +4 -1
- package/build/engine/game-loop.d.ts +3 -1
- package/build/engine/game-loop.js +11 -8
- package/build/engine/math-lib/math/ops.d.ts +16 -0
- package/build/engine/math-lib/math/ops.js +25 -0
- package/build/engine/scene/scene-manager.d.ts +4 -1
- package/build/engine/scene/scene-manager.js +5 -1
- package/build/engine/system/index.d.ts +1 -1
- package/build/engine/system/system.d.ts +10 -11
- package/build/engine/system/system.js +5 -2
- package/build/engine/time.d.ts +54 -0
- package/build/engine/time.js +77 -0
- package/build/index.d.ts +2 -1
- package/package.json +1 -1
- package/build/contrib/systems/physics-system/subsystems/collision-detection/dispersion-calculator/index.d.ts +0 -11
- package/build/contrib/systems/physics-system/subsystems/collision-detection/dispersion-calculator/index.js +0 -41
- package/build/contrib/utils/one-way-validator.d.ts +0 -12
- package/build/engine/data-lib/sort/index.d.ts +0 -1
- package/build/engine/data-lib/sort/index.js +0 -1
- package/build/engine/data-lib/sort/insertion-sort.d.ts +0 -1
- package/build/engine/data-lib/sort/insertion-sort.js +0 -14
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export const getContactRestitution = (rigidBody1, rigidBody2) => {
|
|
2
|
+
return Math.max(rigidBody1.restitution, rigidBody2.restitution);
|
|
3
|
+
};
|
|
4
|
+
export const getContactFriction = (rigidBody1, rigidBody2) => {
|
|
5
|
+
return Math.sqrt(rigidBody1.friction * rigidBody2.friction);
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Returns the effective mass at a contact point along a unit direction.
|
|
9
|
+
* It represents how strongly the two bodies resist an impulse applied at
|
|
10
|
+
* that point in that direction.
|
|
11
|
+
*/
|
|
12
|
+
export const getEffectiveMass = (invMassA, invInertiaA, anchorAX, anchorAY, invMassB, invInertiaB, anchorBX, anchorBY, directionX, directionY) => {
|
|
13
|
+
const crossA = anchorAX * directionY - anchorAY * directionX;
|
|
14
|
+
const crossB = anchorBX * directionY - anchorBY * directionX;
|
|
15
|
+
return (invMassA +
|
|
16
|
+
invMassB +
|
|
17
|
+
crossA * crossA * invInertiaA +
|
|
18
|
+
crossB * crossB * invInertiaB);
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Relative velocity of body B against body A at a contact point, projected onto
|
|
22
|
+
* a direction, using the supplied velocity fields (live, previous, or bias).
|
|
23
|
+
*/
|
|
24
|
+
const getRelativeVelocityAtPoint = (linearVelocityA, angularVelocityA, linearVelocityB, angularVelocityB, point, directionX, directionY) => {
|
|
25
|
+
const velocityAX = linearVelocityA.x - angularVelocityA * point.anchorAY;
|
|
26
|
+
const velocityAY = linearVelocityA.y + angularVelocityA * point.anchorAX;
|
|
27
|
+
const velocityBX = linearVelocityB.x - angularVelocityB * point.anchorBY;
|
|
28
|
+
const velocityBY = linearVelocityB.y + angularVelocityB * point.anchorBX;
|
|
29
|
+
return ((velocityBX - velocityAX) * directionX +
|
|
30
|
+
(velocityBY - velocityAY) * directionY);
|
|
31
|
+
};
|
|
32
|
+
export const getNormalVelocity = (state, point) => getRelativeVelocityAtPoint(state.bodyA.linearVelocity, state.bodyA.angularVelocity, state.bodyB.linearVelocity, state.bodyB.angularVelocity, point, state.normalX, state.normalY);
|
|
33
|
+
export const getTangentVelocity = (state, point) => getRelativeVelocityAtPoint(state.bodyA.linearVelocity, state.bodyA.angularVelocity, state.bodyB.linearVelocity, state.bodyB.angularVelocity, point, state.tangentX, state.tangentY);
|
|
34
|
+
export const getPrevNormalVelocity = (state, point) => getRelativeVelocityAtPoint(state.bodyA._prevLinearVelocity, state.bodyA._prevAngularVelocity, state.bodyB._prevLinearVelocity, state.bodyB._prevAngularVelocity, point, state.normalX, state.normalY);
|
|
35
|
+
export const getBiasNormalVelocity = (state, point) => getRelativeVelocityAtPoint(state.bodyA._biasLinearVelocity, state.bodyA._biasAngularVelocity, state.bodyB._biasLinearVelocity, state.bodyB._biasAngularVelocity, point, state.normalX, state.normalY);
|
|
36
|
+
export const applyImpulse = (state, point, impulseX, impulseY) => {
|
|
37
|
+
const { bodyA, bodyB } = state;
|
|
38
|
+
if (state.invMassA > 0) {
|
|
39
|
+
bodyA.linearVelocity.x -= impulseX * state.invMassA;
|
|
40
|
+
bodyA.linearVelocity.y -= impulseY * state.invMassA;
|
|
41
|
+
}
|
|
42
|
+
if (state.invInertiaA > 0) {
|
|
43
|
+
bodyA.angularVelocity -=
|
|
44
|
+
(point.anchorAX * impulseY - point.anchorAY * impulseX) *
|
|
45
|
+
state.invInertiaA;
|
|
46
|
+
}
|
|
47
|
+
if (state.invMassB > 0) {
|
|
48
|
+
bodyB.linearVelocity.x += impulseX * state.invMassB;
|
|
49
|
+
bodyB.linearVelocity.y += impulseY * state.invMassB;
|
|
50
|
+
}
|
|
51
|
+
if (state.invInertiaB > 0) {
|
|
52
|
+
bodyB.angularVelocity +=
|
|
53
|
+
(point.anchorBX * impulseY - point.anchorBY * impulseX) *
|
|
54
|
+
state.invInertiaB;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
export const applyBiasImpulse = (state, point, impulseX, impulseY) => {
|
|
58
|
+
const { bodyA, bodyB } = state;
|
|
59
|
+
if (state.invMassA > 0) {
|
|
60
|
+
bodyA._biasLinearVelocity.x -= impulseX * state.invMassA;
|
|
61
|
+
bodyA._biasLinearVelocity.y -= impulseY * state.invMassA;
|
|
62
|
+
}
|
|
63
|
+
if (state.invInertiaA > 0) {
|
|
64
|
+
bodyA._biasAngularVelocity -=
|
|
65
|
+
(point.anchorAX * impulseY - point.anchorAY * impulseX) *
|
|
66
|
+
state.invInertiaA;
|
|
67
|
+
}
|
|
68
|
+
if (state.invMassB > 0) {
|
|
69
|
+
bodyB._biasLinearVelocity.x += impulseX * state.invMassB;
|
|
70
|
+
bodyB._biasLinearVelocity.y += impulseY * state.invMassB;
|
|
71
|
+
}
|
|
72
|
+
if (state.invInertiaB > 0) {
|
|
73
|
+
bodyB._biasAngularVelocity +=
|
|
74
|
+
(point.anchorBX * impulseY - point.anchorBY * impulseX) *
|
|
75
|
+
state.invInertiaB;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
@@ -1,13 +1,62 @@
|
|
|
1
|
+
import type { Time } from '../../../../../engine/time';
|
|
2
|
+
import { type Vector2 } from '../../../../../engine/math-lib';
|
|
1
3
|
import type { Contact } from '../collision-detection/types';
|
|
4
|
+
interface ConstraintSolverOptions {
|
|
5
|
+
time: Time;
|
|
6
|
+
getGravity: () => Vector2;
|
|
7
|
+
solverIterations?: number;
|
|
8
|
+
maxAllowedPenetration?: number;
|
|
9
|
+
maxBiasVelocity?: number;
|
|
10
|
+
linearSleepThreshold?: number;
|
|
11
|
+
}
|
|
2
12
|
export declare class ConstraintSolver {
|
|
3
|
-
private validContacts;
|
|
4
13
|
private oneWayValidator;
|
|
5
|
-
|
|
14
|
+
private contactStateManager;
|
|
15
|
+
private sleepSupportTracker;
|
|
16
|
+
private time;
|
|
17
|
+
private getGravity;
|
|
18
|
+
private restitutionThreshold;
|
|
19
|
+
private solverIterations;
|
|
20
|
+
private maxAllowedPenetration;
|
|
21
|
+
private maxBiasVelocity;
|
|
22
|
+
private linearSleepThreshold;
|
|
23
|
+
private penetrationSleepThreshold;
|
|
24
|
+
private flippedNormal;
|
|
25
|
+
private blockImpulse0;
|
|
26
|
+
private blockImpulse1;
|
|
27
|
+
constructor(options: ConstraintSolverOptions);
|
|
6
28
|
private validateCollision;
|
|
7
29
|
private validateOneWayContact;
|
|
8
|
-
private
|
|
30
|
+
private validateSleepContact;
|
|
31
|
+
private getPoint;
|
|
32
|
+
private prepareContact;
|
|
33
|
+
private prepareBlockSolver;
|
|
34
|
+
private prepareDecisions;
|
|
35
|
+
private applyWarmStartImpulse;
|
|
9
36
|
private applyNormalImpulse;
|
|
37
|
+
private applyPointNormalImpulse;
|
|
38
|
+
private applyBlockNormalImpulse;
|
|
39
|
+
/**
|
|
40
|
+
* Solves the normal impulses for both contact points of a two-point
|
|
41
|
+
* manifold together, instead of one at a time.
|
|
42
|
+
*
|
|
43
|
+
* A contact point can only push, never pull (its impulse
|
|
44
|
+
* must stay >= 0), and a point that's already pulling apart on its own
|
|
45
|
+
* shouldn't be pushed at all. Which point(s) actually need pushing this
|
|
46
|
+
* step isn't known upfront, so this tries the possibilities from most to
|
|
47
|
+
* least likely and uses the first one that comes out consistent:
|
|
48
|
+
* 1. Both points push — solve the 2x2 system directly; only valid if both
|
|
49
|
+
* resulting impulses are non-negative.
|
|
50
|
+
* 2. Only point0 pushes, point1 is left alone — only valid if point1
|
|
51
|
+
* isn't still closing in without help (that would mean it needed a
|
|
52
|
+
* push too, contradicting the assumption).
|
|
53
|
+
* 3. Only point1 pushes, point0 is left alone — mirror of case 2.
|
|
54
|
+
* 4. Neither pushes — only valid if both points are already separating
|
|
55
|
+
* on their own.
|
|
56
|
+
*/
|
|
57
|
+
private solveBlockNormalImpulse;
|
|
10
58
|
private applyFrictionImpulse;
|
|
11
|
-
private
|
|
59
|
+
private applyBiasImpulse;
|
|
12
60
|
update(contacts: Contact[]): void;
|
|
13
61
|
}
|
|
62
|
+
export {};
|
|
@@ -1,16 +1,57 @@
|
|
|
1
|
+
import { MathOps, } from '../../../../../engine/math-lib';
|
|
1
2
|
import { RigidBody } from '../../../../components/rigid-body';
|
|
2
3
|
import { Transform } from '../../../../components/transform';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
import { DEFAULT_SOLVER_ITERATIONS, DEFAULT_CONTACT_MAX_ALLOWED_PENETRATION, CONTACT_BIAS, DEFAULT_MAX_BIAS_VELOCITY, RESTITUTION_VELOCITY_THRESHOLD, DEFAULT_LINEAR_SLEEP_THRESHOLD, PENETRATION_SLEEP_MARGIN, } from '../../consts';
|
|
5
|
+
import { ContactStateManager, } from './contact-state-manager';
|
|
6
|
+
import { OneWayValidator } from './one-way-validator';
|
|
7
|
+
import { getContactFriction, getContactRestitution, getEffectiveMass, getNormalVelocity, getTangentVelocity, getPrevNormalVelocity, getBiasNormalVelocity, applyImpulse, applyBiasImpulse, } from './impulse-utils';
|
|
8
|
+
import { shouldWakeSleepingContact } from './contact-utils';
|
|
9
|
+
import { SleepSupportTracker } from './sleep-support-tracker';
|
|
10
|
+
const BLOCK_SOLVER_MIN_DETERMINANT = 1e-8;
|
|
11
|
+
const BLOCK_SOLVER_TOLERANCE = 1e-8;
|
|
12
|
+
/**
|
|
13
|
+
* A resting body gains g * dt of downward velocity from gravity before the
|
|
14
|
+
* solver runs. The restitution threshold must be higher than this velocity,
|
|
15
|
+
* otherwise resting contacts may be treated as impacts and keep bouncing.
|
|
16
|
+
*
|
|
17
|
+
* This factor keeps the threshold safely above one frame of gravity while
|
|
18
|
+
* never allowing it to fall below the base threshold.
|
|
19
|
+
*/
|
|
20
|
+
const RESTITUTION_GRAVITY_THRESHOLD_FACTOR = 2;
|
|
8
21
|
export class ConstraintSolver {
|
|
9
|
-
validContacts;
|
|
10
22
|
oneWayValidator;
|
|
11
|
-
|
|
12
|
-
|
|
23
|
+
contactStateManager;
|
|
24
|
+
sleepSupportTracker;
|
|
25
|
+
time;
|
|
26
|
+
getGravity;
|
|
27
|
+
restitutionThreshold;
|
|
28
|
+
solverIterations;
|
|
29
|
+
maxAllowedPenetration;
|
|
30
|
+
maxBiasVelocity;
|
|
31
|
+
linearSleepThreshold;
|
|
32
|
+
penetrationSleepThreshold;
|
|
33
|
+
flippedNormal;
|
|
34
|
+
blockImpulse0;
|
|
35
|
+
blockImpulse1;
|
|
36
|
+
constructor(options) {
|
|
13
37
|
this.oneWayValidator = new OneWayValidator();
|
|
38
|
+
this.contactStateManager = new ContactStateManager();
|
|
39
|
+
this.sleepSupportTracker = new SleepSupportTracker(options.getGravity);
|
|
40
|
+
this.time = options.time;
|
|
41
|
+
this.getGravity = options.getGravity;
|
|
42
|
+
this.restitutionThreshold = RESTITUTION_VELOCITY_THRESHOLD;
|
|
43
|
+
this.solverIterations =
|
|
44
|
+
options.solverIterations ?? DEFAULT_SOLVER_ITERATIONS;
|
|
45
|
+
this.maxAllowedPenetration =
|
|
46
|
+
options.maxAllowedPenetration ?? DEFAULT_CONTACT_MAX_ALLOWED_PENETRATION;
|
|
47
|
+
this.maxBiasVelocity = options.maxBiasVelocity ?? DEFAULT_MAX_BIAS_VELOCITY;
|
|
48
|
+
this.linearSleepThreshold =
|
|
49
|
+
options.linearSleepThreshold ?? DEFAULT_LINEAR_SLEEP_THRESHOLD;
|
|
50
|
+
this.penetrationSleepThreshold =
|
|
51
|
+
this.maxAllowedPenetration + PENETRATION_SLEEP_MARGIN;
|
|
52
|
+
this.flippedNormal = { x: 0, y: 0 };
|
|
53
|
+
this.blockImpulse0 = 0;
|
|
54
|
+
this.blockImpulse1 = 0;
|
|
14
55
|
}
|
|
15
56
|
validateCollision(actor1, actor2) {
|
|
16
57
|
const rigidBody1 = actor1.getComponent(RigidBody);
|
|
@@ -30,124 +71,292 @@ export class ConstraintSolver {
|
|
|
30
71
|
const rigidBody1 = contact.actor1.getComponent(RigidBody);
|
|
31
72
|
const rigidBody2 = contact.actor2.getComponent(RigidBody);
|
|
32
73
|
if (rigidBody1.oneWay &&
|
|
33
|
-
|
|
74
|
+
rigidBody2.type === 'dynamic' &&
|
|
75
|
+
!this.oneWayValidator.shouldBlock(contact.actor1, contact.actor2, contact.normal)) {
|
|
34
76
|
return false;
|
|
35
77
|
}
|
|
36
|
-
if (!rigidBody2.oneWay) {
|
|
78
|
+
if (!rigidBody2.oneWay || rigidBody1.type !== 'dynamic') {
|
|
37
79
|
return true;
|
|
38
80
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
});
|
|
81
|
+
this.flippedNormal.x = -contact.normal.x;
|
|
82
|
+
this.flippedNormal.y = -contact.normal.y;
|
|
83
|
+
return this.oneWayValidator.shouldBlock(contact.actor2, contact.actor1, this.flippedNormal);
|
|
43
84
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
85
|
+
validateSleepContact(contact) {
|
|
86
|
+
const rigidBody1 = contact.actor1.getComponent(RigidBody);
|
|
87
|
+
const rigidBody2 = contact.actor2.getComponent(RigidBody);
|
|
88
|
+
const isDeepContact = contact.penetration > this.penetrationSleepThreshold;
|
|
89
|
+
const isFastContact = shouldWakeSleepingContact(contact, this.linearSleepThreshold);
|
|
90
|
+
if (!isDeepContact && !isFastContact) {
|
|
91
|
+
this.sleepSupportTracker.trackContact(contact);
|
|
47
92
|
}
|
|
48
|
-
|
|
93
|
+
if (rigidBody1.sleeping && rigidBody2.sleeping) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
if ((rigidBody1.sleeping || rigidBody2.sleeping) &&
|
|
97
|
+
(isDeepContact || isFastContact)) {
|
|
98
|
+
rigidBody1.wakeUp();
|
|
99
|
+
rigidBody2.wakeUp();
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
49
102
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return impulseMagnitude;
|
|
78
|
-
}
|
|
79
|
-
applyFrictionImpulse(contact, normalImpulseMagnitude) {
|
|
80
|
-
if (normalImpulseMagnitude <= 0) {
|
|
103
|
+
getPoint(state, index) {
|
|
104
|
+
return index === 0 ? state.point0 : state.point1;
|
|
105
|
+
}
|
|
106
|
+
prepareContact(state) {
|
|
107
|
+
const { normal } = state.contact;
|
|
108
|
+
const bodyA = state.actor1.getComponent(RigidBody);
|
|
109
|
+
const bodyB = state.actor2.getComponent(RigidBody);
|
|
110
|
+
const transformA = state.actor1.getComponent(Transform);
|
|
111
|
+
const transformB = state.actor2.getComponent(Transform);
|
|
112
|
+
const invMassA = bodyA.inverseMass;
|
|
113
|
+
const invMassB = bodyB.inverseMass;
|
|
114
|
+
const invInertiaA = bodyA.inverseInertia;
|
|
115
|
+
const invInertiaB = bodyB.inverseInertia;
|
|
116
|
+
state.bodyA = bodyA;
|
|
117
|
+
state.bodyB = bodyB;
|
|
118
|
+
state.invMassA = invMassA;
|
|
119
|
+
state.invInertiaA = invInertiaA;
|
|
120
|
+
state.invMassB = invMassB;
|
|
121
|
+
state.invInertiaB = invInertiaB;
|
|
122
|
+
state.normalX = normal.x;
|
|
123
|
+
state.normalY = normal.y;
|
|
124
|
+
state.tangentX = -normal.y;
|
|
125
|
+
state.tangentY = normal.x;
|
|
126
|
+
state.friction = getContactFriction(bodyA, bodyB);
|
|
127
|
+
state.restitution = getContactRestitution(bodyA, bodyB);
|
|
128
|
+
state.active = invMassA + invMassB + invInertiaA + invInertiaB > 0;
|
|
129
|
+
if (!state.active) {
|
|
81
130
|
return;
|
|
82
131
|
}
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
132
|
+
const posAX = transformA.world.position.x;
|
|
133
|
+
const posAY = transformA.world.position.y;
|
|
134
|
+
const posBX = transformB.world.position.x;
|
|
135
|
+
const posBY = transformB.world.position.y;
|
|
136
|
+
let minPrevNormalVelocity = Infinity;
|
|
137
|
+
for (let index = 0; index < state.pointCount; index += 1) {
|
|
138
|
+
const point = this.getPoint(state, index);
|
|
139
|
+
point.anchorAX = point.positionX - posAX;
|
|
140
|
+
point.anchorAY = point.positionY - posAY;
|
|
141
|
+
point.anchorBX = point.positionX - posBX;
|
|
142
|
+
point.anchorBY = point.positionY - posBY;
|
|
143
|
+
point.normalMass = getEffectiveMass(invMassA, invInertiaA, point.anchorAX, point.anchorAY, invMassB, invInertiaB, point.anchorBX, point.anchorBY, state.normalX, state.normalY);
|
|
144
|
+
point.tangentMass = getEffectiveMass(invMassA, invInertiaA, point.anchorAX, point.anchorAY, invMassB, invInertiaB, point.anchorBX, point.anchorBY, state.tangentX, state.tangentY);
|
|
145
|
+
const prevNormalVelocity = getPrevNormalVelocity(state, point);
|
|
146
|
+
if (prevNormalVelocity < minPrevNormalVelocity) {
|
|
147
|
+
minPrevNormalVelocity = prevNormalVelocity;
|
|
148
|
+
}
|
|
149
|
+
if (state.restitution > 0 &&
|
|
150
|
+
-prevNormalVelocity > this.restitutionThreshold) {
|
|
151
|
+
point.velocityBias = -state.restitution * prevNormalVelocity;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
point.velocityBias = 0;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
this.prepareBlockSolver(state);
|
|
158
|
+
this.prepareDecisions(state, minPrevNormalVelocity);
|
|
159
|
+
}
|
|
160
|
+
prepareBlockSolver(state) {
|
|
161
|
+
if (state.pointCount !== 2 ||
|
|
162
|
+
state.point0.normalMass === 0 ||
|
|
163
|
+
state.point1.normalMass === 0) {
|
|
164
|
+
state.blockSolvable = false;
|
|
90
165
|
return;
|
|
91
166
|
}
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
167
|
+
const { point0, point1 } = state;
|
|
168
|
+
const crossA0 = point0.anchorAX * state.normalY - point0.anchorAY * state.normalX;
|
|
169
|
+
const crossB0 = point0.anchorBX * state.normalY - point0.anchorBY * state.normalX;
|
|
170
|
+
const crossA1 = point1.anchorAX * state.normalY - point1.anchorAY * state.normalX;
|
|
171
|
+
const crossB1 = point1.anchorBX * state.normalY - point1.anchorBY * state.normalX;
|
|
172
|
+
state.k01 =
|
|
173
|
+
state.invMassA +
|
|
174
|
+
state.invMassB +
|
|
175
|
+
crossA0 * crossA1 * state.invInertiaA +
|
|
176
|
+
crossB0 * crossB1 * state.invInertiaB;
|
|
177
|
+
state.determinant =
|
|
178
|
+
point0.normalMass * point1.normalMass - state.k01 * state.k01;
|
|
179
|
+
state.blockSolvable =
|
|
180
|
+
Math.abs(state.determinant) >= BLOCK_SOLVER_MIN_DETERMINANT;
|
|
181
|
+
}
|
|
182
|
+
prepareDecisions(state, minPrevNormalVelocity) {
|
|
183
|
+
const hasRestitution = state.restitution > 0;
|
|
184
|
+
const isBouncing = hasRestitution && -minPrevNormalVelocity > this.restitutionThreshold;
|
|
185
|
+
const eitherSleeping = state.bodyA.sleeping || state.bodyB.sleeping;
|
|
186
|
+
state.warmStartAllowed =
|
|
187
|
+
(!hasRestitution || !isBouncing) && !eitherSleeping;
|
|
188
|
+
state.skipBias =
|
|
189
|
+
isBouncing && (state.invMassA === 0 || state.invMassB === 0);
|
|
190
|
+
}
|
|
191
|
+
applyWarmStartImpulse(state) {
|
|
192
|
+
if (!state.warmStartAllowed) {
|
|
193
|
+
this.contactStateManager.clearWarmStartImpulses(state);
|
|
99
194
|
return;
|
|
100
195
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (inverseMass2 > 0) {
|
|
115
|
-
rigidBody2.linearVelocity.x += impulseX * inverseMass2;
|
|
116
|
-
rigidBody2.linearVelocity.y += impulseY * inverseMass2;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
applyPositionCorrection(contact) {
|
|
120
|
-
const { actor1, actor2, normal, penetration } = contact;
|
|
121
|
-
const rigidBody1 = actor1.getComponent(RigidBody);
|
|
122
|
-
const rigidBody2 = actor2.getComponent(RigidBody);
|
|
123
|
-
const inverseMass1 = this.getInverseMass(rigidBody1);
|
|
124
|
-
const inverseMass2 = this.getInverseMass(rigidBody2);
|
|
125
|
-
const inverseMassSum = inverseMass1 + inverseMass2;
|
|
126
|
-
if (inverseMassSum === 0) {
|
|
196
|
+
for (let index = 0; index < state.pointCount; index += 1) {
|
|
197
|
+
const point = this.getPoint(state, index);
|
|
198
|
+
if (point.normalImpulse === 0 && point.tangentImpulse === 0) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
applyImpulse(state, point, state.normalX * point.normalImpulse +
|
|
202
|
+
state.tangentX * point.tangentImpulse, state.normalY * point.normalImpulse +
|
|
203
|
+
state.tangentY * point.tangentImpulse);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
applyNormalImpulse(state) {
|
|
207
|
+
if (state.blockSolvable) {
|
|
208
|
+
this.applyBlockNormalImpulse(state);
|
|
127
209
|
return;
|
|
128
210
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
211
|
+
for (let index = 0; index < state.pointCount; index += 1) {
|
|
212
|
+
this.applyPointNormalImpulse(state, index);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
applyPointNormalImpulse(state, index) {
|
|
216
|
+
const point = this.getPoint(state, index);
|
|
217
|
+
if (point.normalMass === 0) {
|
|
133
218
|
return;
|
|
134
219
|
}
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
220
|
+
const velocityAlongNormal = getNormalVelocity(state, point);
|
|
221
|
+
const oldImpulse = point.normalImpulse;
|
|
222
|
+
const newImpulse = Math.max(oldImpulse +
|
|
223
|
+
(point.velocityBias - velocityAlongNormal) / point.normalMass, 0);
|
|
224
|
+
point.normalImpulse = newImpulse;
|
|
225
|
+
const impulse = newImpulse - oldImpulse;
|
|
226
|
+
if (impulse === 0) {
|
|
227
|
+
return;
|
|
142
228
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
229
|
+
applyImpulse(state, point, state.normalX * impulse, state.normalY * impulse);
|
|
230
|
+
}
|
|
231
|
+
applyBlockNormalImpulse(state) {
|
|
232
|
+
const { point0, point1 } = state;
|
|
233
|
+
const mass00 = point0.normalMass;
|
|
234
|
+
const mass11 = point1.normalMass;
|
|
235
|
+
const mass01 = state.k01;
|
|
236
|
+
const velocity0 = getNormalVelocity(state, point0) - point0.velocityBias;
|
|
237
|
+
const velocity1 = getNormalVelocity(state, point1) - point1.velocityBias;
|
|
238
|
+
const oldImpulse0 = point0.normalImpulse;
|
|
239
|
+
const oldImpulse1 = point1.normalImpulse;
|
|
240
|
+
const target0 = mass00 * oldImpulse0 + mass01 * oldImpulse1 - velocity0;
|
|
241
|
+
const target1 = mass01 * oldImpulse0 + mass11 * oldImpulse1 - velocity1;
|
|
242
|
+
if (!this.solveBlockNormalImpulse(mass00, mass01, mass11, target0, target1, velocity0, velocity1, oldImpulse0, oldImpulse1, state.determinant)) {
|
|
243
|
+
this.applyPointNormalImpulse(state, 0);
|
|
244
|
+
this.applyPointNormalImpulse(state, 1);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const impulseDelta0 = this.blockImpulse0 - oldImpulse0;
|
|
248
|
+
const impulseDelta1 = this.blockImpulse1 - oldImpulse1;
|
|
249
|
+
point0.normalImpulse = this.blockImpulse0;
|
|
250
|
+
point1.normalImpulse = this.blockImpulse1;
|
|
251
|
+
if (impulseDelta0 !== 0) {
|
|
252
|
+
applyImpulse(state, point0, state.normalX * impulseDelta0, state.normalY * impulseDelta0);
|
|
253
|
+
}
|
|
254
|
+
if (impulseDelta1 !== 0) {
|
|
255
|
+
applyImpulse(state, point1, state.normalX * impulseDelta1, state.normalY * impulseDelta1);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Solves the normal impulses for both contact points of a two-point
|
|
260
|
+
* manifold together, instead of one at a time.
|
|
261
|
+
*
|
|
262
|
+
* A contact point can only push, never pull (its impulse
|
|
263
|
+
* must stay >= 0), and a point that's already pulling apart on its own
|
|
264
|
+
* shouldn't be pushed at all. Which point(s) actually need pushing this
|
|
265
|
+
* step isn't known upfront, so this tries the possibilities from most to
|
|
266
|
+
* least likely and uses the first one that comes out consistent:
|
|
267
|
+
* 1. Both points push — solve the 2x2 system directly; only valid if both
|
|
268
|
+
* resulting impulses are non-negative.
|
|
269
|
+
* 2. Only point0 pushes, point1 is left alone — only valid if point1
|
|
270
|
+
* isn't still closing in without help (that would mean it needed a
|
|
271
|
+
* push too, contradicting the assumption).
|
|
272
|
+
* 3. Only point1 pushes, point0 is left alone — mirror of case 2.
|
|
273
|
+
* 4. Neither pushes — only valid if both points are already separating
|
|
274
|
+
* on their own.
|
|
275
|
+
*/
|
|
276
|
+
solveBlockNormalImpulse(mass00, mass01, mass11, target0, target1, velocity0, velocity1, oldImpulse0, oldImpulse1, determinant) {
|
|
277
|
+
const bothImpulse0 = (mass11 * target0 - mass01 * target1) / determinant;
|
|
278
|
+
const bothImpulse1 = (mass00 * target1 - mass01 * target0) / determinant;
|
|
279
|
+
if (bothImpulse0 >= 0 && bothImpulse1 >= 0) {
|
|
280
|
+
this.blockImpulse0 = bothImpulse0;
|
|
281
|
+
this.blockImpulse1 = bothImpulse1;
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
const point0Impulse = target0 / mass00;
|
|
285
|
+
const point0InactiveVelocity = velocity1 + mass01 * (point0Impulse - oldImpulse0) - mass11 * oldImpulse1;
|
|
286
|
+
if (point0Impulse >= 0 &&
|
|
287
|
+
point0InactiveVelocity >= -BLOCK_SOLVER_TOLERANCE) {
|
|
288
|
+
this.blockImpulse0 = point0Impulse;
|
|
289
|
+
this.blockImpulse1 = 0;
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
const point1Impulse = target1 / mass11;
|
|
293
|
+
const point1InactiveVelocity = velocity0 - mass00 * oldImpulse0 + mass01 * (point1Impulse - oldImpulse1);
|
|
294
|
+
if (point1Impulse >= 0 &&
|
|
295
|
+
point1InactiveVelocity >= -BLOCK_SOLVER_TOLERANCE) {
|
|
296
|
+
this.blockImpulse0 = 0;
|
|
297
|
+
this.blockImpulse1 = point1Impulse;
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
const inactiveVelocity0 = velocity0 - mass00 * oldImpulse0 - mass01 * oldImpulse1;
|
|
301
|
+
const inactiveVelocity1 = velocity1 - mass01 * oldImpulse0 - mass11 * oldImpulse1;
|
|
302
|
+
if (inactiveVelocity0 >= -BLOCK_SOLVER_TOLERANCE &&
|
|
303
|
+
inactiveVelocity1 >= -BLOCK_SOLVER_TOLERANCE) {
|
|
304
|
+
this.blockImpulse0 = 0;
|
|
305
|
+
this.blockImpulse1 = 0;
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
applyFrictionImpulse(state) {
|
|
311
|
+
for (let index = 0; index < state.pointCount; index += 1) {
|
|
312
|
+
const point = this.getPoint(state, index);
|
|
313
|
+
if (point.tangentMass === 0) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const maxFrictionImpulse = state.friction * point.normalImpulse;
|
|
317
|
+
const velocityAlongTangent = getTangentVelocity(state, point);
|
|
318
|
+
const oldImpulse = point.tangentImpulse;
|
|
319
|
+
const newImpulse = MathOps.clamp(oldImpulse - velocityAlongTangent / point.tangentMass, -maxFrictionImpulse, maxFrictionImpulse);
|
|
320
|
+
point.tangentImpulse = newImpulse;
|
|
321
|
+
const impulse = newImpulse - oldImpulse;
|
|
322
|
+
if (impulse === 0) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
applyImpulse(state, point, state.tangentX * impulse, state.tangentY * impulse);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
applyBiasImpulse(state, deltaTime) {
|
|
329
|
+
const targetBiasVelocity = Math.min((Math.max(state.contact.penetration - this.maxAllowedPenetration, 0) *
|
|
330
|
+
CONTACT_BIAS) /
|
|
331
|
+
deltaTime, this.maxBiasVelocity);
|
|
332
|
+
if (targetBiasVelocity === 0) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
for (let index = 0; index < state.pointCount; index += 1) {
|
|
336
|
+
const point = this.getPoint(state, index);
|
|
337
|
+
if (point.normalMass === 0) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const biasVelocityAlongNormal = getBiasNormalVelocity(state, point);
|
|
341
|
+
const oldImpulse = point.biasImpulse;
|
|
342
|
+
const newImpulse = Math.max(oldImpulse +
|
|
343
|
+
(targetBiasVelocity - biasVelocityAlongNormal) / point.normalMass, 0);
|
|
344
|
+
point.biasImpulse = newImpulse;
|
|
345
|
+
const impulse = newImpulse - oldImpulse;
|
|
346
|
+
if (impulse === 0) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
applyBiasImpulse(state, point, state.normalX * impulse, state.normalY * impulse);
|
|
146
350
|
}
|
|
147
351
|
}
|
|
148
352
|
update(contacts) {
|
|
149
|
-
this.oneWayValidator.
|
|
150
|
-
|
|
353
|
+
this.oneWayValidator.updateVersion();
|
|
354
|
+
this.contactStateManager.updateVersion();
|
|
355
|
+
this.sleepSupportTracker.beginFrame();
|
|
356
|
+
const deltaTime = this.time.fixedDeltaTime;
|
|
357
|
+
this.restitutionThreshold = Math.max(RESTITUTION_VELOCITY_THRESHOLD, RESTITUTION_GRAVITY_THRESHOLD_FACTOR *
|
|
358
|
+
this.getGravity().magnitude *
|
|
359
|
+
deltaTime);
|
|
151
360
|
contacts.forEach((contact) => {
|
|
152
361
|
if (!this.validateCollision(contact.actor1, contact.actor2)) {
|
|
153
362
|
return;
|
|
@@ -155,19 +364,28 @@ export class ConstraintSolver {
|
|
|
155
364
|
if (!this.validateOneWayContact(contact)) {
|
|
156
365
|
return;
|
|
157
366
|
}
|
|
158
|
-
this.
|
|
159
|
-
|
|
367
|
+
if (!this.validateSleepContact(contact)) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const state = this.contactStateManager.acquire(contact);
|
|
371
|
+
this.prepareContact(state);
|
|
372
|
+
this.applyWarmStartImpulse(state);
|
|
160
373
|
});
|
|
161
|
-
this.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
374
|
+
this.sleepSupportTracker.wakeUnsupportedBodies();
|
|
375
|
+
this.contactStateManager.pruneStaleStates();
|
|
376
|
+
for (let iteration = 0; iteration < this.solverIterations; iteration += 1) {
|
|
377
|
+
this.contactStateManager.forEach((state) => {
|
|
378
|
+
if (!state.active) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
this.applyNormalImpulse(state);
|
|
382
|
+
this.applyFrictionImpulse(state);
|
|
383
|
+
if (!state.skipBias) {
|
|
384
|
+
this.applyBiasImpulse(state, deltaTime);
|
|
385
|
+
}
|
|
166
386
|
});
|
|
167
387
|
}
|
|
168
|
-
this.
|
|
169
|
-
|
|
170
|
-
});
|
|
171
|
-
this.oneWayValidator.lateUpdate();
|
|
388
|
+
this.oneWayValidator.clearOneWayContacts();
|
|
389
|
+
this.sleepSupportTracker.endFrame();
|
|
172
390
|
}
|
|
173
391
|
}
|
package/build/contrib/systems/physics-system/subsystems/constraint-solver/one-way-validator.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Actor } from '../../../../../engine/actor';
|
|
2
|
+
import { type Point } from '../../../../../engine/math-lib';
|
|
3
|
+
export declare class OneWayValidator {
|
|
4
|
+
private ignoredOneWayContacts;
|
|
5
|
+
private version;
|
|
6
|
+
constructor();
|
|
7
|
+
private trackOneWayContact;
|
|
8
|
+
shouldBlock(oneWayActor: Actor, otherActor: Actor, normal: Point): boolean;
|
|
9
|
+
updateVersion(): void;
|
|
10
|
+
clearOneWayContacts(): void;
|
|
11
|
+
}
|