2026-07-24 14:02:24 +00:00
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
|
|
#include "vde/brep/assembly_constraints.h"
|
|
|
|
|
|
#include "vde/brep/feature_tree.h"
|
|
|
|
|
|
#include "vde/brep/modeling.h"
|
|
|
|
|
|
#include "vde/core/transform.h"
|
|
|
|
|
|
#include <cmath>
|
|
|
|
|
|
|
|
|
|
|
|
using namespace vde::brep;
|
|
|
|
|
|
using namespace vde::core;
|
|
|
|
|
|
|
|
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// Assembly Constraint Tests
|
|
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
TEST(AssemblyConstraintTest, CoincidentConstraint) {
|
|
|
|
|
|
Assembly assy("test_assy");
|
|
|
|
|
|
auto* box_a = assy.root.add_part("box_a", make_box(2, 2, 2));
|
|
|
|
|
|
auto* box_b = assy.root.add_part("box_b", make_box(2, 2, 2),
|
|
|
|
|
|
translate(0, 0, 10));
|
|
|
|
|
|
|
|
|
|
|
|
EXPECT_TRUE(apply_constraint(assy, box_a, box_b, ConstraintType::Coincident));
|
|
|
|
|
|
|
|
|
|
|
|
// After coincident constraint, box_b should be sitting directly on box_a
|
|
|
|
|
|
// Verify by checking that the bounding boxes share a face plane
|
|
|
|
|
|
AABB3D bb_a = box_a->model->bounds();
|
|
|
|
|
|
// Compute box_b's world-space bounding box
|
|
|
|
|
|
AABB3D bb_b_local = box_b->model->bounds();
|
|
|
|
|
|
AABB3D bb_b_world;
|
|
|
|
|
|
Point3D corners[8] = {
|
|
|
|
|
|
bb_b_local.min(),
|
|
|
|
|
|
Point3D(bb_b_local.max().x(), bb_b_local.min().y(), bb_b_local.min().z()),
|
|
|
|
|
|
Point3D(bb_b_local.max().x(), bb_b_local.max().y(), bb_b_local.min().z()),
|
|
|
|
|
|
Point3D(bb_b_local.min().x(), bb_b_local.max().y(), bb_b_local.min().z()),
|
|
|
|
|
|
Point3D(bb_b_local.min().x(), bb_b_local.min().y(), bb_b_local.max().z()),
|
|
|
|
|
|
Point3D(bb_b_local.max().x(), bb_b_local.min().y(), bb_b_local.max().z()),
|
|
|
|
|
|
bb_b_local.max(),
|
|
|
|
|
|
Point3D(bb_b_local.min().x(), bb_b_local.max().y(), bb_b_local.max().z()),
|
|
|
|
|
|
};
|
|
|
|
|
|
for (const auto& c : corners) {
|
|
|
|
|
|
bb_b_world.expand(box_b->local_transform * c);
|
|
|
|
|
|
}
|
|
|
|
|
|
// box_a top = 1.0, box_b bottom should be very close to 1.0
|
|
|
|
|
|
EXPECT_NEAR(bb_b_world.min().z(), bb_a.max().z(), 1e-4);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TEST(AssemblyConstraintTest, ConcentricConstraint) {
|
|
|
|
|
|
Assembly assy("test_assy");
|
|
|
|
|
|
auto* cyl_a = assy.root.add_part("cyl_a", make_cylinder(1.0, 4.0));
|
|
|
|
|
|
auto* cyl_b = assy.root.add_part("cyl_b", make_cylinder(0.5, 2.0),
|
|
|
|
|
|
translate(5, 0, 0));
|
|
|
|
|
|
|
|
|
|
|
|
EXPECT_TRUE(apply_constraint(assy, cyl_a, cyl_b, ConstraintType::Concentric));
|
|
|
|
|
|
|
|
|
|
|
|
// Both cylinders should now be concentric (same center in X,Y)
|
|
|
|
|
|
AABB3D bb_a = cyl_a->model->bounds();
|
|
|
|
|
|
Point3D center_a = bb_a.center();
|
|
|
|
|
|
|
|
|
|
|
|
AABB3D bb_b_local = cyl_b->model->bounds();
|
|
|
|
|
|
Point3D center_b_local = bb_b_local.center();
|
|
|
|
|
|
Point3D center_b_world = cyl_b->local_transform * center_b_local;
|
|
|
|
|
|
|
|
|
|
|
|
EXPECT_NEAR(center_b_world.x(), center_a.x(), 1e-4);
|
|
|
|
|
|
EXPECT_NEAR(center_b_world.y(), center_a.y(), 1e-4);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TEST(AssemblyConstraintTest, DistanceConstraint) {
|
|
|
|
|
|
Assembly assy("test_assy");
|
|
|
|
|
|
auto* box_a = assy.root.add_part("box_a", make_box(2, 2, 2));
|
|
|
|
|
|
auto* box_b = assy.root.add_part("box_b", make_box(2, 2, 2),
|
|
|
|
|
|
translate(0, 0, 10));
|
|
|
|
|
|
|
|
|
|
|
|
EXPECT_TRUE(apply_constraint(assy, box_a, box_b, ConstraintType::Distance, 5.0));
|
|
|
|
|
|
|
|
|
|
|
|
// After distance constraint of 5.0, box_b bottom should be 5.0 above box_a top
|
|
|
|
|
|
AABB3D bb_a = box_a->model->bounds();
|
|
|
|
|
|
AABB3D bb_b_local = box_b->model->bounds();
|
|
|
|
|
|
|
|
|
|
|
|
Point3D bb_b_min_local = bb_b_local.min();
|
|
|
|
|
|
Point3D bb_b_min_world = box_b->local_transform * bb_b_min_local;
|
|
|
|
|
|
|
|
|
|
|
|
double gap = bb_b_min_world.z() - bb_a.max().z();
|
|
|
|
|
|
EXPECT_NEAR(gap, 5.0, 1e-4);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TEST(AssemblyConstraintTest, AngleConstraint) {
|
|
|
|
|
|
Assembly assy("test_assy");
|
|
|
|
|
|
auto* box_a = assy.root.add_part("box_a", make_box(2, 2, 2));
|
|
|
|
|
|
auto* box_b = assy.root.add_part("box_b", make_box(1, 1, 4));
|
|
|
|
|
|
|
|
|
|
|
|
// Apply 90-degree angle constraint (rotate node_b)
|
|
|
|
|
|
EXPECT_TRUE(apply_constraint(assy, box_a, box_b, ConstraintType::Angle, M_PI / 2.0));
|
|
|
|
|
|
|
|
|
|
|
|
// Verify the transform was applied (non-identity rotation)
|
|
|
|
|
|
EXPECT_FALSE(box_b->local_transform.isApprox(Transform3D::Identity(), 1e-6));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TEST(AssemblyConstraintTest, PerpendicularConstraint) {
|
|
|
|
|
|
Assembly assy("test_assy");
|
|
|
|
|
|
auto* box_a = assy.root.add_part("box_a", make_box(2, 2, 2));
|
|
|
|
|
|
auto* box_b = assy.root.add_part("box_b", make_box(1, 1, 4));
|
|
|
|
|
|
|
|
|
|
|
|
EXPECT_TRUE(apply_constraint(assy, box_a, box_b, ConstraintType::Perpendicular));
|
|
|
|
|
|
|
|
|
|
|
|
// Verify the transform is not identity (rotation was applied)
|
|
|
|
|
|
EXPECT_FALSE(box_b->local_transform.isApprox(Transform3D::Identity(), 1e-6));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TEST(AssemblyConstraintTest, ParallelConstraint) {
|
|
|
|
|
|
Assembly assy("test_assy");
|
|
|
|
|
|
auto* box_a = assy.root.add_part("box_a", make_box(2, 2, 2));
|
|
|
|
|
|
auto* box_b = assy.root.add_part("box_b", make_box(2, 2, 2),
|
|
|
|
|
|
translate(0, 5, 0));
|
|
|
|
|
|
|
|
|
|
|
|
EXPECT_TRUE(apply_constraint(assy, box_a, box_b, ConstraintType::Parallel));
|
|
|
|
|
|
|
|
|
|
|
|
// Parallel constraint preserves the offset (no transform change for axis-aligned)
|
|
|
|
|
|
AABB3D bb_b_local = box_b->model->bounds();
|
|
|
|
|
|
Point3D center_b_local = bb_b_local.center();
|
|
|
|
|
|
Point3D center_b_world = box_b->local_transform * center_b_local;
|
|
|
|
|
|
EXPECT_NEAR(center_b_world.y(), 5.0, 1e-4);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// Feature Tree Tests
|
|
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
TEST(FeatureTreeTest, PrimitiveBox) {
|
|
|
|
|
|
FeatureNode node;
|
|
|
|
|
|
node.type = FeatureType::PrimitiveBox;
|
|
|
|
|
|
node.params.values = {2.0, 3.0, 4.0};
|
|
|
|
|
|
|
|
|
|
|
|
BrepModel result = node.evaluate();
|
|
|
|
|
|
EXPECT_GT(result.num_vertices(), 0u);
|
|
|
|
|
|
EXPECT_GT(result.num_faces(), 0u);
|
|
|
|
|
|
|
|
|
|
|
|
AABB3D bb = result.bounds();
|
|
|
|
|
|
EXPECT_NEAR(bb.extent().x(), 2.0, 1e-4);
|
|
|
|
|
|
EXPECT_NEAR(bb.extent().y(), 3.0, 1e-4);
|
|
|
|
|
|
EXPECT_NEAR(bb.extent().z(), 4.0, 1e-4);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TEST(FeatureTreeTest, PrimitiveCylinder) {
|
|
|
|
|
|
FeatureNode node;
|
|
|
|
|
|
node.type = FeatureType::PrimitiveCylinder;
|
|
|
|
|
|
node.params.values = {1.5, 6.0};
|
|
|
|
|
|
|
|
|
|
|
|
BrepModel result = node.evaluate();
|
|
|
|
|
|
EXPECT_GT(result.num_vertices(), 0u);
|
|
|
|
|
|
EXPECT_GT(result.num_faces(), 0u);
|
|
|
|
|
|
|
|
|
|
|
|
AABB3D bb = result.bounds();
|
|
|
|
|
|
EXPECT_NEAR(bb.extent().x(), 3.0, 0.1); // diameter
|
2026-07-25 01:43:28 +00:00
|
|
|
|
EXPECT_NEAR(bb.extent().z(), 6.0, 1e-4); // height (cylinder is along Z axis)
|
2026-07-24 14:02:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TEST(FeatureTreeTest, PrimitiveSphere) {
|
|
|
|
|
|
FeatureNode node;
|
|
|
|
|
|
node.type = FeatureType::PrimitiveSphere;
|
|
|
|
|
|
node.params.values = {2.5};
|
|
|
|
|
|
|
|
|
|
|
|
BrepModel result = node.evaluate();
|
|
|
|
|
|
EXPECT_GT(result.num_vertices(), 0u);
|
|
|
|
|
|
|
|
|
|
|
|
AABB3D bb = result.bounds();
|
|
|
|
|
|
EXPECT_NEAR(bb.extent().x(), 5.0, 0.1); // diameter
|
|
|
|
|
|
EXPECT_NEAR(bb.extent().y(), 5.0, 0.1);
|
|
|
|
|
|
EXPECT_NEAR(bb.extent().z(), 5.0, 0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TEST(FeatureTreeTest, NestedFeatureTree) {
|
|
|
|
|
|
// Box → Fillet
|
|
|
|
|
|
auto box_node = std::make_unique<FeatureNode>();
|
|
|
|
|
|
box_node->type = FeatureType::PrimitiveBox;
|
|
|
|
|
|
box_node->params.values = {10.0, 5.0, 3.0};
|
|
|
|
|
|
|
|
|
|
|
|
auto fillet_node = std::make_unique<FeatureNode>();
|
|
|
|
|
|
fillet_node->type = FeatureType::Fillet;
|
|
|
|
|
|
fillet_node->params.values = {1.0};
|
|
|
|
|
|
fillet_node->params.int_values = {0};
|
|
|
|
|
|
fillet_node->children.push_back(std::move(box_node));
|
|
|
|
|
|
|
|
|
|
|
|
BrepModel result = fillet_node->evaluate();
|
|
|
|
|
|
EXPECT_TRUE(result.is_valid());
|
|
|
|
|
|
EXPECT_GT(result.num_faces(), 0u);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TEST(FeatureTreeTest, BooleanUnionFeature) {
|
|
|
|
|
|
// Box ∪ Cylinder
|
|
|
|
|
|
auto box = std::make_unique<FeatureNode>();
|
|
|
|
|
|
box->type = FeatureType::PrimitiveBox;
|
|
|
|
|
|
box->params.values = {3.0, 3.0, 3.0};
|
|
|
|
|
|
|
|
|
|
|
|
auto cyl = std::make_unique<FeatureNode>();
|
|
|
|
|
|
cyl->type = FeatureType::PrimitiveCylinder;
|
|
|
|
|
|
cyl->params.values = {1.0, 5.0};
|
|
|
|
|
|
|
|
|
|
|
|
auto union_node = std::make_unique<FeatureNode>();
|
|
|
|
|
|
union_node->type = FeatureType::BooleanUnion;
|
|
|
|
|
|
union_node->children.push_back(std::move(box));
|
|
|
|
|
|
union_node->children.push_back(std::move(cyl));
|
|
|
|
|
|
|
|
|
|
|
|
BrepModel result = union_node->evaluate();
|
|
|
|
|
|
EXPECT_GT(result.num_vertices(), 0u);
|
|
|
|
|
|
EXPECT_GT(result.num_faces(), 0u);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// FeatureHistory Undo/Redo Tests
|
|
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
TEST(FeatureHistoryTest, ApplyOperations) {
|
|
|
|
|
|
FeatureHistory history;
|
|
|
|
|
|
|
|
|
|
|
|
// Apply 3 operations
|
|
|
|
|
|
BrepModel box1 = make_box(2, 2, 2);
|
|
|
|
|
|
history.apply(box1, FeatureType::PrimitiveBox, FeatureParams{});
|
|
|
|
|
|
EXPECT_EQ(history.size(), 1u);
|
|
|
|
|
|
|
|
|
|
|
|
BrepModel box2 = make_box(3, 3, 3);
|
|
|
|
|
|
history.apply(box2, FeatureType::PrimitiveBox, FeatureParams{});
|
|
|
|
|
|
EXPECT_EQ(history.size(), 2u);
|
|
|
|
|
|
|
|
|
|
|
|
BrepModel cyl = make_cylinder(1, 5);
|
|
|
|
|
|
history.apply(cyl, FeatureType::PrimitiveCylinder, FeatureParams{});
|
|
|
|
|
|
EXPECT_EQ(history.size(), 3u);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TEST(FeatureHistoryTest, UndoRedo) {
|
|
|
|
|
|
FeatureHistory history;
|
|
|
|
|
|
|
|
|
|
|
|
// Create 3 distinct models as initial states
|
|
|
|
|
|
BrepModel box_small = make_box(2, 2, 2);
|
|
|
|
|
|
BrepModel box_large = make_box(5, 5, 5);
|
|
|
|
|
|
BrepModel cylinder = make_cylinder(2, 10);
|
|
|
|
|
|
|
|
|
|
|
|
history.apply(box_small, FeatureType::PrimitiveBox, FeatureParams{});
|
|
|
|
|
|
EXPECT_EQ(history.size(), 1u);
|
|
|
|
|
|
|
|
|
|
|
|
history.apply(box_large, FeatureType::PrimitiveBox, FeatureParams{});
|
|
|
|
|
|
EXPECT_EQ(history.size(), 2u);
|
|
|
|
|
|
|
|
|
|
|
|
history.apply(cylinder, FeatureType::PrimitiveCylinder, FeatureParams{});
|
|
|
|
|
|
EXPECT_EQ(history.size(), 3u);
|
|
|
|
|
|
|
|
|
|
|
|
// Undo twice
|
|
|
|
|
|
EXPECT_TRUE(history.undo());
|
|
|
|
|
|
EXPECT_EQ(history.size(), 3u); // size doesn't change with undo
|
|
|
|
|
|
|
|
|
|
|
|
EXPECT_TRUE(history.undo());
|
|
|
|
|
|
EXPECT_EQ(history.size(), 3u);
|
|
|
|
|
|
|
|
|
|
|
|
// Should not be able to undo past start
|
|
|
|
|
|
EXPECT_TRUE(history.undo());
|
|
|
|
|
|
EXPECT_FALSE(history.undo()); // no more to undo
|
|
|
|
|
|
|
|
|
|
|
|
// Redo all
|
|
|
|
|
|
EXPECT_TRUE(history.redo());
|
|
|
|
|
|
EXPECT_TRUE(history.redo());
|
|
|
|
|
|
EXPECT_TRUE(history.redo());
|
|
|
|
|
|
EXPECT_FALSE(history.redo()); // no more to redo
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TEST(FeatureHistoryTest, UndoRedoVerifyStates) {
|
|
|
|
|
|
FeatureHistory history;
|
|
|
|
|
|
|
|
|
|
|
|
BrepModel initial = make_box(2, 2, 2);
|
|
|
|
|
|
history.apply(initial, FeatureType::PrimitiveBox, FeatureParams{});
|
|
|
|
|
|
|
|
|
|
|
|
BrepModel second = make_box(5, 5, 5);
|
|
|
|
|
|
history.apply(second, FeatureType::PrimitiveBox, FeatureParams{});
|
|
|
|
|
|
|
|
|
|
|
|
BrepModel third = make_cylinder(2, 10);
|
|
|
|
|
|
history.apply(third, FeatureType::PrimitiveCylinder, FeatureParams{});
|
|
|
|
|
|
|
|
|
|
|
|
// Current should be the third operation's state
|
|
|
|
|
|
AABB3D bb_current = history.current().bounds();
|
|
|
|
|
|
EXPECT_NEAR(bb_current.extent().x(), 4.0, 0.5); // cylinder diameter
|
|
|
|
|
|
|
|
|
|
|
|
// Undo → should get second state
|
|
|
|
|
|
EXPECT_TRUE(history.undo());
|
|
|
|
|
|
AABB3D bb_undo = history.current().bounds();
|
|
|
|
|
|
EXPECT_NEAR(bb_undo.extent().x(), 5.0, 1e-4);
|
|
|
|
|
|
|
|
|
|
|
|
// Redo → should get third state back
|
|
|
|
|
|
EXPECT_TRUE(history.redo());
|
|
|
|
|
|
AABB3D bb_redo = history.current().bounds();
|
|
|
|
|
|
EXPECT_NEAR(bb_redo.extent().x(), 4.0, 0.5);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TEST(FeatureHistoryTest, ApplyAfterUndoTruncatesRedo) {
|
|
|
|
|
|
FeatureHistory history;
|
|
|
|
|
|
|
|
|
|
|
|
history.apply(make_box(2, 2, 2), FeatureType::PrimitiveBox, FeatureParams{});
|
|
|
|
|
|
history.apply(make_box(5, 5, 5), FeatureType::PrimitiveBox, FeatureParams{});
|
|
|
|
|
|
history.apply(make_cylinder(2, 10), FeatureType::PrimitiveCylinder, FeatureParams{});
|
|
|
|
|
|
|
|
|
|
|
|
// Undo one
|
|
|
|
|
|
EXPECT_TRUE(history.undo());
|
|
|
|
|
|
EXPECT_EQ(history.size(), 3u);
|
|
|
|
|
|
|
|
|
|
|
|
// Apply a new operation → should truncate redo stack
|
|
|
|
|
|
history.apply(make_box(10, 10, 10), FeatureType::PrimitiveBox, FeatureParams{});
|
|
|
|
|
|
EXPECT_EQ(history.size(), 3u); // still 3 → old "third" was replaced
|
|
|
|
|
|
|
|
|
|
|
|
// Redo should be exhausted (old third was deleted)
|
|
|
|
|
|
EXPECT_FALSE(history.redo());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TEST(FeatureHistoryTest, ThreeOperationsUndoRedoVerify) {
|
|
|
|
|
|
FeatureHistory history;
|
|
|
|
|
|
|
|
|
|
|
|
// Apply op1: small box
|
|
|
|
|
|
history.apply(make_box(2, 2, 2), FeatureType::PrimitiveBox, FeatureParams{});
|
|
|
|
|
|
|
|
|
|
|
|
// Apply op2: large box
|
|
|
|
|
|
history.apply(make_box(5, 5, 5), FeatureType::PrimitiveBox, FeatureParams{});
|
|
|
|
|
|
|
|
|
|
|
|
// Apply op3: cylinder
|
|
|
|
|
|
history.apply(make_cylinder(2, 10), FeatureType::PrimitiveCylinder, FeatureParams{});
|
|
|
|
|
|
|
|
|
|
|
|
EXPECT_EQ(history.size(), 3u);
|
|
|
|
|
|
|
|
|
|
|
|
// Verify current is cylinder
|
|
|
|
|
|
{
|
|
|
|
|
|
AABB3D bb = history.current().bounds();
|
|
|
|
|
|
EXPECT_NEAR(bb.extent().x(), 4.0, 0.5); // cylinder diameter ≈ 4
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Undo back to large box
|
|
|
|
|
|
EXPECT_TRUE(history.undo());
|
|
|
|
|
|
{
|
|
|
|
|
|
AABB3D bb = history.current().bounds();
|
|
|
|
|
|
EXPECT_NEAR(bb.extent().x(), 5.0, 1e-4);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Undo back to small box
|
|
|
|
|
|
EXPECT_TRUE(history.undo());
|
|
|
|
|
|
{
|
|
|
|
|
|
AABB3D bb = history.current().bounds();
|
|
|
|
|
|
EXPECT_NEAR(bb.extent().x(), 2.0, 1e-4);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Undo back to empty
|
|
|
|
|
|
EXPECT_TRUE(history.undo());
|
|
|
|
|
|
EXPECT_FALSE(history.undo());
|
|
|
|
|
|
|
|
|
|
|
|
// Redo: empty → small box → large box → cylinder
|
|
|
|
|
|
EXPECT_TRUE(history.redo());
|
|
|
|
|
|
EXPECT_TRUE(history.redo());
|
|
|
|
|
|
EXPECT_TRUE(history.redo());
|
|
|
|
|
|
EXPECT_FALSE(history.redo());
|
|
|
|
|
|
|
|
|
|
|
|
// Verify we're back at cylinder
|
|
|
|
|
|
{
|
|
|
|
|
|
AABB3D bb = history.current().bounds();
|
|
|
|
|
|
EXPECT_NEAR(bb.extent().x(), 4.0, 0.5);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|