#include #include "vde/brep/explode_view.h" #include "vde/brep/modeling.h" #include "vde/core/transform.h" #include using namespace vde::brep; using namespace vde::core; // ═══════════════════════════════════════════════════════════ // Basic explosion // ═══════════════════════════════════════════════════════════ TEST(ExplodeViewTest, SinglePart_NoExplosion) { Assembly assy("single"); assy.root.add_part("box", make_box(2, 2, 2)); ExplodeConfig cfg; cfg.factor = 1.0; cfg.use_constraint_dirs = false; auto result = explode_view(assy, cfg); EXPECT_GE(result.steps.size(), 1u); EXPECT_GT(result.max_displacement(), 0.0); } TEST(ExplodeViewTest, TwoParts_DisplaceAway) { Assembly assy("two"); assy.root.add_part("left", make_box(2, 2, 2), translate(-3, 0, 0)); assy.root.add_part("right", make_box(2, 2, 2), translate(3, 0, 0)); ExplodeConfig cfg; cfg.factor = 2.0; auto result = explode_view(assy, cfg); EXPECT_EQ(result.steps.size(), 2u); // Both parts should move further apart EXPECT_GT(result.max_displacement(), 3.0); // factor 2 × original offset 3 } TEST(ExplodeViewTest, Explode_ExpandsBounds) { Assembly assy("expand"); assy.root.add_part("a", make_box(2, 2, 2), translate(0, -3, 0)); assy.root.add_part("b", make_box(2, 2, 2), translate(0, 3, 0)); double orig_extent = assy.root.children[0]->model->bounds().extent().norm(); ExplodeConfig cfg; cfg.factor = 2.0; auto result = explode_view(assy, cfg); Vector3D expl_ext = result.exploded_bounds.extent(); // Exploded bounds should be larger than any single part EXPECT_GT(expl_ext.norm(), orig_extent); } TEST(ExplodeViewTest, UnExplode_RestoresTransforms) { Assembly assy("restore"); assy.root.add_part("part", make_box(2, 2, 2), translate(5, 0, 0)); // Save original transform Transform3D orig_tf = assy.root.children[0]->local_transform; ExplodeConfig cfg; cfg.factor = 3.0; auto result = explode_view(assy, cfg); // Transform should have changed bool changed = assy.root.children[0]->local_transform.matrix() != orig_tf.matrix(); EXPECT_TRUE(changed); // Un-explode un_explode(assy, result); // Should be back to original bool restored = assy.root.children[0]->local_transform.matrix() == orig_tf.matrix(); EXPECT_TRUE(restored); } TEST(ExplodeViewTest, AutoDirection_ChoosesLongestAxis) { Assembly assy("auto_dir"); // Create a wide but flat assembly assy.root.add_part("wide", make_box(10, 1, 1)); ExplodeConfig cfg; cfg.factor = 1.0; cfg.use_constraint_dirs = false; // direction is zero → auto auto result = explode_view(assy, cfg); // Should pick X axis (longest extent) // Verify by checking that displacement is primarily along X for (auto& step : result.steps) { Vector3D d = step.displacement; EXPECT_GE(std::abs(d.x()), std::abs(d.y())); EXPECT_GE(std::abs(d.x()), std::abs(d.z())); } } TEST(ExplodeViewTest, ExplicitDirection_Used) { Assembly assy("explicit_dir"); assy.root.add_part("box", make_box(2, 2, 2), translate(0, 3, 0)); ExplodeConfig cfg; cfg.factor = 2.0; cfg.direction = Vector3D(0, 0, 1); // explicit +Z direction cfg.use_constraint_dirs = false; auto result = explode_view(assy, cfg); for (auto& step : result.steps) { Vector3D d = step.displacement; // Displacement should be primarily along Z EXPECT_GE(std::abs(d.z()), std::abs(d.x())); EXPECT_GE(std::abs(d.z()), std::abs(d.y())); } } TEST(ExplodeViewTest, FactorScalesDisplacement) { Assembly assy("scale"); assy.root.add_part("part", make_box(2, 2, 2), translate(0, 4, 0)); ExplodeConfig cfg1; cfg1.factor = 1.0; auto r1 = explode_view(assy, cfg1); // Reset un_explode(assy, r1); ExplodeConfig cfg2; cfg2.factor = 3.0; auto r2 = explode_view(assy, cfg2); // Factor 3 should give roughly 3× the displacement EXPECT_NEAR(r2.max_displacement() / r1.max_displacement(), 3.0, 0.5); } TEST(ExplodeViewTest, Subassembly_NotExplodedInternally) { Assembly assy("parent"); auto* sub = assy.root.add_subassembly("group", translate(0, 5, 0)); sub->add_part("sub_a", make_box(1, 1, 1), translate(-1, 0, 0)); sub->add_part("sub_b", make_box(1, 1, 1), translate(1, 0, 0)); assy.root.add_part("main", make_box(3, 1, 3)); ExplodeConfig cfg; cfg.factor = 2.0; cfg.preserve_subassemblies = true; auto result = explode_view(assy, cfg); // Should still work without crash EXPECT_GE(result.steps.size(), 1u); } TEST(ExplodeViewTest, MaxDisplacement_NonNegative) { Assembly assy("max_disp"); assy.root.add_part("a", make_box(2, 2, 2)); auto result = explode_view(assy); EXPECT_GE(result.max_displacement(), 0.0); } TEST(ExplodeViewTest, EmptyAssembly_NoSteps) { Assembly assy("empty"); auto result = explode_view(assy); EXPECT_EQ(result.steps.size(), 0u); EXPECT_EQ(result.max_displacement(), 0.0); } TEST(ExplodeViewTest, ThreePartsChain_AllDisplaced) { Assembly assy("chain"); assy.root.add_part("root", make_box(3, 1, 1)); assy.root.add_part("middle", make_box(2, 1, 1), translate(4, 0, 0)); assy.root.add_part("tip", make_box(1, 1, 1), translate(7, 0, 0)); ExplodeConfig cfg; cfg.factor = 1.5; auto result = explode_view(assy, cfg); EXPECT_EQ(result.steps.size(), 3u); // Each part should have been displaced for (auto& step : result.steps) { EXPECT_GT(step.displacement.norm(), 0.0); } } TEST(ExplodeViewTest, ComputedDisplacement_DirectionConsistent) { Assembly assy("consistent"); // Place parts in a line assy.root.add_part("p0", make_box(2, 2, 2), translate(0, 5, 0)); assy.root.add_part("p1", make_box(2, 2, 2), translate(0, 10, 0)); ExplodeConfig cfg; cfg.factor = 2.0; auto result = explode_view(assy, cfg); ASSERT_GE(result.steps.size(), 2u); // All displacements should be in roughly the same direction (away from origin) Vector3D d0 = result.steps[0].displacement.normalized(); Vector3D d1 = result.steps[1].displacement.normalized(); EXPECT_GT(d0.dot(d1), 0.5); // should point in similar direction }