a2b34733bf
- explode_view() with auto/manual direction + factor control - un_explode() for restoring original transforms - compute_explode_displacement() for per-part displacement - 12 tests: single part, chain, factor scaling, un-explode, subassembly
206 lines
6.4 KiB
C++
206 lines
6.4 KiB
C++
#include <gtest/gtest.h>
|
||
#include "vde/brep/explode_view.h"
|
||
#include "vde/brep/modeling.h"
|
||
#include "vde/core/transform.h"
|
||
#include <cmath>
|
||
|
||
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
|
||
}
|