|
@@ -1,9 +1,9 @@
|
|
|
-#include "render/humanoid/pose_controller.h"
|
|
|
|
|
#include "render/humanoid/humanoid_specs.h"
|
|
#include "render/humanoid/humanoid_specs.h"
|
|
|
|
|
+#include "render/humanoid/pose_controller.h"
|
|
|
#include "render/humanoid/rig.h"
|
|
#include "render/humanoid/rig.h"
|
|
|
#include <QVector3D>
|
|
#include <QVector3D>
|
|
|
-#include <gtest/gtest.h>
|
|
|
|
|
#include <cmath>
|
|
#include <cmath>
|
|
|
|
|
+#include <gtest/gtest.h>
|
|
|
|
|
|
|
|
using namespace Render::GL;
|
|
using namespace Render::GL;
|
|
|
|
|
|
|
@@ -39,7 +39,7 @@ protected:
|
|
|
|
|
|
|
|
HumanoidPose pose;
|
|
HumanoidPose pose;
|
|
|
HumanoidAnimationContext anim_ctx;
|
|
HumanoidAnimationContext anim_ctx;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Helper to check if a position is approximately equal
|
|
// Helper to check if a position is approximately equal
|
|
|
bool approxEqual(const QVector3D &a, const QVector3D &b,
|
|
bool approxEqual(const QVector3D &a, const QVector3D &b,
|
|
|
float epsilon = 0.01F) {
|
|
float epsilon = 0.01F) {
|
|
@@ -58,12 +58,12 @@ TEST_F(HumanoidPoseControllerTest, ConstructorInitializesCorrectly) {
|
|
|
|
|
|
|
|
TEST_F(HumanoidPoseControllerTest, StandIdleDoesNotModifyPose) {
|
|
TEST_F(HumanoidPoseControllerTest, StandIdleDoesNotModifyPose) {
|
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
QVector3D const original_pelvis = pose.pelvisPos;
|
|
QVector3D const original_pelvis = pose.pelvisPos;
|
|
|
QVector3D const original_shoulder_l = pose.shoulderL;
|
|
QVector3D const original_shoulder_l = pose.shoulderL;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
controller.standIdle();
|
|
controller.standIdle();
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// standIdle should be a no-op, keeping pose unchanged
|
|
// standIdle should be a no-op, keeping pose unchanged
|
|
|
EXPECT_TRUE(approxEqual(pose.pelvisPos, original_pelvis));
|
|
EXPECT_TRUE(approxEqual(pose.pelvisPos, original_pelvis));
|
|
|
EXPECT_TRUE(approxEqual(pose.shoulderL, original_shoulder_l));
|
|
EXPECT_TRUE(approxEqual(pose.shoulderL, original_shoulder_l));
|
|
@@ -71,14 +71,14 @@ TEST_F(HumanoidPoseControllerTest, StandIdleDoesNotModifyPose) {
|
|
|
|
|
|
|
|
TEST_F(HumanoidPoseControllerTest, KneelLowersPelvis) {
|
|
TEST_F(HumanoidPoseControllerTest, KneelLowersPelvis) {
|
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
float const original_pelvis_y = pose.pelvisPos.y();
|
|
float const original_pelvis_y = pose.pelvisPos.y();
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
controller.kneel(0.5F);
|
|
controller.kneel(0.5F);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Kneeling should lower the pelvis
|
|
// Kneeling should lower the pelvis
|
|
|
EXPECT_LT(pose.pelvisPos.y(), original_pelvis_y);
|
|
EXPECT_LT(pose.pelvisPos.y(), original_pelvis_y);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Pelvis should be lowered by approximately depth * 0.40F
|
|
// Pelvis should be lowered by approximately depth * 0.40F
|
|
|
float const expected_offset = 0.5F * 0.40F;
|
|
float const expected_offset = 0.5F * 0.40F;
|
|
|
EXPECT_NEAR(pose.pelvisPos.y(), HumanProportions::WAIST_Y - expected_offset,
|
|
EXPECT_NEAR(pose.pelvisPos.y(), HumanProportions::WAIST_Y - expected_offset,
|
|
@@ -87,36 +87,36 @@ TEST_F(HumanoidPoseControllerTest, KneelLowersPelvis) {
|
|
|
|
|
|
|
|
TEST_F(HumanoidPoseControllerTest, KneelFullDepthTouchesGroundWithKnee) {
|
|
TEST_F(HumanoidPoseControllerTest, KneelFullDepthTouchesGroundWithKnee) {
|
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
controller.kneel(1.0F);
|
|
controller.kneel(1.0F);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// At full kneel, left knee should be very close to ground
|
|
// At full kneel, left knee should be very close to ground
|
|
|
EXPECT_NEAR(pose.knee_l.y(), HumanProportions::GROUND_Y + 0.07F, 0.02F);
|
|
EXPECT_NEAR(pose.knee_l.y(), HumanProportions::GROUND_Y + 0.07F, 0.02F);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Pelvis should be lowered significantly
|
|
// Pelvis should be lowered significantly
|
|
|
EXPECT_LT(pose.pelvisPos.y(), HumanProportions::WAIST_Y - 0.35F);
|
|
EXPECT_LT(pose.pelvisPos.y(), HumanProportions::WAIST_Y - 0.35F);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
TEST_F(HumanoidPoseControllerTest, KneelZeroDepthKeepsStanding) {
|
|
TEST_F(HumanoidPoseControllerTest, KneelZeroDepthKeepsStanding) {
|
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
float const original_pelvis_y = pose.pelvisPos.y();
|
|
float const original_pelvis_y = pose.pelvisPos.y();
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
controller.kneel(0.0F);
|
|
controller.kneel(0.0F);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Zero depth should keep pelvis at original height
|
|
// Zero depth should keep pelvis at original height
|
|
|
EXPECT_NEAR(pose.pelvisPos.y(), original_pelvis_y, 0.01F);
|
|
EXPECT_NEAR(pose.pelvisPos.y(), original_pelvis_y, 0.01F);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
TEST_F(HumanoidPoseControllerTest, LeanMovesUpperBody) {
|
|
TEST_F(HumanoidPoseControllerTest, LeanMovesUpperBody) {
|
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
QVector3D const original_shoulder_l = pose.shoulderL;
|
|
QVector3D const original_shoulder_l = pose.shoulderL;
|
|
|
QVector3D const original_shoulder_r = pose.shoulderR;
|
|
QVector3D const original_shoulder_r = pose.shoulderR;
|
|
|
QVector3D const lean_direction(0.0F, 0.0F, 1.0F); // Forward
|
|
QVector3D const lean_direction(0.0F, 0.0F, 1.0F); // Forward
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
controller.lean(lean_direction, 0.5F);
|
|
controller.lean(lean_direction, 0.5F);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Shoulders should move forward when leaning forward
|
|
// Shoulders should move forward when leaning forward
|
|
|
EXPECT_GT(pose.shoulderL.z(), original_shoulder_l.z());
|
|
EXPECT_GT(pose.shoulderL.z(), original_shoulder_l.z());
|
|
|
EXPECT_GT(pose.shoulderR.z(), original_shoulder_r.z());
|
|
EXPECT_GT(pose.shoulderR.z(), original_shoulder_r.z());
|
|
@@ -124,38 +124,38 @@ TEST_F(HumanoidPoseControllerTest, LeanMovesUpperBody) {
|
|
|
|
|
|
|
|
TEST_F(HumanoidPoseControllerTest, LeanZeroAmountNoChange) {
|
|
TEST_F(HumanoidPoseControllerTest, LeanZeroAmountNoChange) {
|
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
QVector3D const original_shoulder_l = pose.shoulderL;
|
|
QVector3D const original_shoulder_l = pose.shoulderL;
|
|
|
QVector3D const lean_direction(1.0F, 0.0F, 0.0F); // Right
|
|
QVector3D const lean_direction(1.0F, 0.0F, 0.0F); // Right
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
controller.lean(lean_direction, 0.0F);
|
|
controller.lean(lean_direction, 0.0F);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Zero amount should keep shoulders unchanged
|
|
// Zero amount should keep shoulders unchanged
|
|
|
EXPECT_TRUE(approxEqual(pose.shoulderL, original_shoulder_l));
|
|
EXPECT_TRUE(approxEqual(pose.shoulderL, original_shoulder_l));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
TEST_F(HumanoidPoseControllerTest, PlaceHandAtSetsHandPosition) {
|
|
TEST_F(HumanoidPoseControllerTest, PlaceHandAtSetsHandPosition) {
|
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
QVector3D const target_position(0.30F, 1.20F, 0.80F);
|
|
QVector3D const target_position(0.30F, 1.20F, 0.80F);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
controller.placeHandAt(false, target_position); // Right hand
|
|
controller.placeHandAt(false, target_position); // Right hand
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Hand should be at target position
|
|
// Hand should be at target position
|
|
|
EXPECT_TRUE(approxEqual(pose.hand_r, target_position));
|
|
EXPECT_TRUE(approxEqual(pose.hand_r, target_position));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
TEST_F(HumanoidPoseControllerTest, PlaceHandAtComputesElbow) {
|
|
TEST_F(HumanoidPoseControllerTest, PlaceHandAtComputesElbow) {
|
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
QVector3D const target_position(0.30F, 1.20F, 0.80F);
|
|
QVector3D const target_position(0.30F, 1.20F, 0.80F);
|
|
|
QVector3D const original_elbow = pose.elbowR;
|
|
QVector3D const original_elbow = pose.elbowR;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
controller.placeHandAt(false, target_position); // Right hand
|
|
controller.placeHandAt(false, target_position); // Right hand
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Elbow should be recomputed (different from original)
|
|
// Elbow should be recomputed (different from original)
|
|
|
EXPECT_FALSE(approxEqual(pose.elbowR, original_elbow));
|
|
EXPECT_FALSE(approxEqual(pose.elbowR, original_elbow));
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Elbow should be between shoulder and hand
|
|
// Elbow should be between shoulder and hand
|
|
|
float const shoulder_to_elbow_dist = (pose.elbowR - pose.shoulderR).length();
|
|
float const shoulder_to_elbow_dist = (pose.elbowR - pose.shoulderR).length();
|
|
|
float const elbow_to_hand_dist = (target_position - pose.elbowR).length();
|
|
float const elbow_to_hand_dist = (target_position - pose.elbowR).length();
|
|
@@ -165,18 +165,17 @@ TEST_F(HumanoidPoseControllerTest, PlaceHandAtComputesElbow) {
|
|
|
|
|
|
|
|
TEST_F(HumanoidPoseControllerTest, SolveElbowIKReturnsValidPosition) {
|
|
TEST_F(HumanoidPoseControllerTest, SolveElbowIKReturnsValidPosition) {
|
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
QVector3D const shoulder = pose.shoulderR;
|
|
QVector3D const shoulder = pose.shoulderR;
|
|
|
QVector3D const hand(0.35F, 1.15F, 0.75F);
|
|
QVector3D const hand(0.35F, 1.15F, 0.75F);
|
|
|
QVector3D const outward_dir(1.0F, 0.0F, 0.0F);
|
|
QVector3D const outward_dir(1.0F, 0.0F, 0.0F);
|
|
|
-
|
|
|
|
|
- QVector3D const elbow = controller.solveElbowIK(false, shoulder, hand,
|
|
|
|
|
- outward_dir, 0.45F, 0.15F,
|
|
|
|
|
- 0.0F, 1.0F);
|
|
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ QVector3D const elbow = controller.solveElbowIK(
|
|
|
|
|
+ false, shoulder, hand, outward_dir, 0.45F, 0.15F, 0.0F, 1.0F);
|
|
|
|
|
+
|
|
|
// Elbow should be somewhere between shoulder and hand
|
|
// Elbow should be somewhere between shoulder and hand
|
|
|
EXPECT_GT(elbow.length(), 0.0F);
|
|
EXPECT_GT(elbow.length(), 0.0F);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Distance from shoulder to elbow should be reasonable
|
|
// Distance from shoulder to elbow should be reasonable
|
|
|
float const shoulder_elbow_dist = (elbow - shoulder).length();
|
|
float const shoulder_elbow_dist = (elbow - shoulder).length();
|
|
|
EXPECT_GT(shoulder_elbow_dist, 0.05F);
|
|
EXPECT_GT(shoulder_elbow_dist, 0.05F);
|
|
@@ -185,31 +184,31 @@ TEST_F(HumanoidPoseControllerTest, SolveElbowIKReturnsValidPosition) {
|
|
|
|
|
|
|
|
TEST_F(HumanoidPoseControllerTest, SolveKneeIKReturnsValidPosition) {
|
|
TEST_F(HumanoidPoseControllerTest, SolveKneeIKReturnsValidPosition) {
|
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
QVector3D const hip(0.10F, 0.93F, 0.0F);
|
|
QVector3D const hip(0.10F, 0.93F, 0.0F);
|
|
|
QVector3D const foot(0.10F, 0.0F, 0.05F);
|
|
QVector3D const foot(0.10F, 0.0F, 0.05F);
|
|
|
float const height_scale = 1.0F;
|
|
float const height_scale = 1.0F;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
QVector3D const knee = controller.solveKneeIK(false, hip, foot, height_scale);
|
|
QVector3D const knee = controller.solveKneeIK(false, hip, foot, height_scale);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Knee should be between hip and foot (in Y)
|
|
// Knee should be between hip and foot (in Y)
|
|
|
EXPECT_LT(knee.y(), hip.y());
|
|
EXPECT_LT(knee.y(), hip.y());
|
|
|
EXPECT_GT(knee.y(), foot.y());
|
|
EXPECT_GT(knee.y(), foot.y());
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Knee should not be below ground
|
|
// Knee should not be below ground
|
|
|
EXPECT_GE(knee.y(), HumanProportions::GROUND_Y);
|
|
EXPECT_GE(knee.y(), HumanProportions::GROUND_Y);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
TEST_F(HumanoidPoseControllerTest, SolveKneeIKPreventsGroundPenetration) {
|
|
TEST_F(HumanoidPoseControllerTest, SolveKneeIKPreventsGroundPenetration) {
|
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Set up a scenario where IK would put knee below ground
|
|
// Set up a scenario where IK would put knee below ground
|
|
|
- QVector3D const hip(0.0F, 0.30F, 0.0F); // Very low hip
|
|
|
|
|
|
|
+ QVector3D const hip(0.0F, 0.30F, 0.0F); // Very low hip
|
|
|
QVector3D const foot(0.50F, 0.0F, 0.50F); // Far foot
|
|
QVector3D const foot(0.50F, 0.0F, 0.50F); // Far foot
|
|
|
float const height_scale = 1.0F;
|
|
float const height_scale = 1.0F;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
QVector3D const knee = controller.solveKneeIK(true, hip, foot, height_scale);
|
|
QVector3D const knee = controller.solveKneeIK(true, hip, foot, height_scale);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Knee should be at or above the floor threshold
|
|
// Knee should be at or above the floor threshold
|
|
|
float const min_knee_y = HumanProportions::GROUND_Y + pose.footYOffset * 0.5F;
|
|
float const min_knee_y = HumanProportions::GROUND_Y + pose.footYOffset * 0.5F;
|
|
|
EXPECT_GE(knee.y(), min_knee_y - 0.001F); // Small epsilon for floating point
|
|
EXPECT_GE(knee.y(), min_knee_y - 0.001F); // Small epsilon for floating point
|
|
@@ -217,52 +216,52 @@ TEST_F(HumanoidPoseControllerTest, SolveKneeIKPreventsGroundPenetration) {
|
|
|
|
|
|
|
|
TEST_F(HumanoidPoseControllerTest, PlaceHandAtLeftHandWorks) {
|
|
TEST_F(HumanoidPoseControllerTest, PlaceHandAtLeftHandWorks) {
|
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
QVector3D const target_position(-0.40F, 1.30F, 0.60F);
|
|
QVector3D const target_position(-0.40F, 1.30F, 0.60F);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
controller.placeHandAt(true, target_position); // Left hand
|
|
controller.placeHandAt(true, target_position); // Left hand
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Left hand should be at target position
|
|
// Left hand should be at target position
|
|
|
EXPECT_TRUE(approxEqual(pose.handL, target_position));
|
|
EXPECT_TRUE(approxEqual(pose.handL, target_position));
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Left elbow should be computed
|
|
// Left elbow should be computed
|
|
|
EXPECT_GT((pose.elbowL - pose.shoulderL).length(), 0.0F);
|
|
EXPECT_GT((pose.elbowL - pose.shoulderL).length(), 0.0F);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
TEST_F(HumanoidPoseControllerTest, KneelClampsBounds) {
|
|
TEST_F(HumanoidPoseControllerTest, KneelClampsBounds) {
|
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Test clamping of depth > 1.0
|
|
// Test clamping of depth > 1.0
|
|
|
controller.kneel(1.5F);
|
|
controller.kneel(1.5F);
|
|
|
float const max_kneel_pelvis_y = pose.pelvisPos.y();
|
|
float const max_kneel_pelvis_y = pose.pelvisPos.y();
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Reset pose
|
|
// Reset pose
|
|
|
SetUp();
|
|
SetUp();
|
|
|
HumanoidPoseController controller2(pose, anim_ctx);
|
|
HumanoidPoseController controller2(pose, anim_ctx);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Test depth = 1.0
|
|
// Test depth = 1.0
|
|
|
controller2.kneel(1.0F);
|
|
controller2.kneel(1.0F);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Should be same as clamped 1.5F
|
|
// Should be same as clamped 1.5F
|
|
|
EXPECT_NEAR(pose.pelvisPos.y(), max_kneel_pelvis_y, 0.001F);
|
|
EXPECT_NEAR(pose.pelvisPos.y(), max_kneel_pelvis_y, 0.001F);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
TEST_F(HumanoidPoseControllerTest, LeanClampsBounds) {
|
|
TEST_F(HumanoidPoseControllerTest, LeanClampsBounds) {
|
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
HumanoidPoseController controller(pose, anim_ctx);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
QVector3D const lean_direction(0.0F, 0.0F, 1.0F);
|
|
QVector3D const lean_direction(0.0F, 0.0F, 1.0F);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Test clamping of amount > 1.0
|
|
// Test clamping of amount > 1.0
|
|
|
controller.lean(lean_direction, 1.5F);
|
|
controller.lean(lean_direction, 1.5F);
|
|
|
float const max_lean_z = pose.shoulderL.z();
|
|
float const max_lean_z = pose.shoulderL.z();
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Reset pose
|
|
// Reset pose
|
|
|
SetUp();
|
|
SetUp();
|
|
|
HumanoidPoseController controller2(pose, anim_ctx);
|
|
HumanoidPoseController controller2(pose, anim_ctx);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Test amount = 1.0
|
|
// Test amount = 1.0
|
|
|
controller2.lean(lean_direction, 1.0F);
|
|
controller2.lean(lean_direction, 1.0F);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Should be same as clamped 1.5F
|
|
// Should be same as clamped 1.5F
|
|
|
EXPECT_NEAR(pose.shoulderL.z(), max_lean_z, 0.001F);
|
|
EXPECT_NEAR(pose.shoulderL.z(), max_lean_z, 0.001F);
|
|
|
}
|
|
}
|