feat(v4.0): explode view — assembly part decomposition
- 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
This commit is contained in:
@@ -148,6 +148,7 @@ add_library(vde_brep STATIC
|
||||
brep/brep.cpp
|
||||
brep/interference_check.cpp
|
||||
brep/motion_simulation.cpp
|
||||
brep/explode_view.cpp
|
||||
brep/modeling.cpp
|
||||
brep/brep_drawing.cpp
|
||||
brep/step_export.cpp
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
#include "vde/brep/explode_view.h"
|
||||
#include "vde/core/aabb.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <queue>
|
||||
|
||||
namespace vde::brep {
|
||||
|
||||
using core::Point3D;
|
||||
using core::Vector3D;
|
||||
using core::Transform3D;
|
||||
using core::AABB3D;
|
||||
|
||||
namespace {
|
||||
|
||||
/// Compute the world-space AABB of an assembly recursively
|
||||
AABB3D assembly_bounds(const AssemblyNode& node, const Transform3D& parent_tf) {
|
||||
Transform3D world_tf = parent_tf * node.local_transform;
|
||||
AABB3D result;
|
||||
|
||||
if (node.is_part() && node.model.has_value()) {
|
||||
AABB3D local = node.model->bounds();
|
||||
if (local.min().x() > local.max().x()) return result;
|
||||
Point3D corners[8] = {
|
||||
local.min(),
|
||||
Point3D(local.max().x(), local.min().y(), local.min().z()),
|
||||
Point3D(local.max().x(), local.max().y(), local.min().z()),
|
||||
Point3D(local.min().x(), local.max().y(), local.min().z()),
|
||||
Point3D(local.min().x(), local.min().y(), local.max().z()),
|
||||
Point3D(local.max().x(), local.min().y(), local.max().z()),
|
||||
local.max(),
|
||||
Point3D(local.min().x(), local.max().y(), local.max().z()),
|
||||
};
|
||||
for (const auto& c : corners) {
|
||||
result.expand(world_tf * c);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& child : node.children) {
|
||||
AABB3D child_bb = assembly_bounds(*child, world_tf);
|
||||
if (child_bb.min().x() <= child_bb.max().x()) result.expand(child_bb.min());
|
||||
if (child_bb.min().x() <= child_bb.max().x()) result.expand(child_bb.max());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Get the world-center of an assembly node
|
||||
Point3D node_world_center(const AssemblyNode& node, const Transform3D& parent_tf) {
|
||||
Transform3D world_tf = parent_tf * node.local_transform;
|
||||
|
||||
if (node.is_part() && node.model.has_value()) {
|
||||
AABB3D local = node.model->bounds();
|
||||
if (local.min().x() <= local.max().x()) {
|
||||
// Just transform the center
|
||||
Point3D center = (local.min() + local.max()) * 0.5;
|
||||
return world_tf * center;
|
||||
}
|
||||
return world_tf * Point3D(0, 0, 0);
|
||||
}
|
||||
|
||||
// For subassemblies, compute aggregate center
|
||||
AABB3D bb = assembly_bounds(node, parent_tf);
|
||||
if (bb.min().x() <= bb.max().x()) {
|
||||
return (bb.min() + bb.max()) * 0.5;
|
||||
}
|
||||
return world_tf * Point3D(0, 0, 0);
|
||||
}
|
||||
|
||||
/// Collect all part nodes with their world transforms and depths
|
||||
struct FlatPart {
|
||||
AssemblyNode* node = nullptr;
|
||||
Transform3D world_tf;
|
||||
std::string name;
|
||||
int depth = 0;
|
||||
};
|
||||
|
||||
void collect_flat_parts(AssemblyNode& root, const Transform3D& parent_tf,
|
||||
int depth, std::vector<FlatPart>& parts) {
|
||||
Transform3D world_tf = parent_tf * root.local_transform;
|
||||
|
||||
if (root.is_part() && root.model.has_value()) {
|
||||
parts.push_back({&root, world_tf, root.name, depth});
|
||||
}
|
||||
|
||||
for (auto& child : root.children) {
|
||||
collect_flat_parts(*child, world_tf, depth + 1, parts);
|
||||
}
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Public API
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
Vector3D compute_explode_displacement(
|
||||
const AssemblyNode& node, const Transform3D& parent_tf, double factor)
|
||||
{
|
||||
if (!node.is_part() || !node.model.has_value()) {
|
||||
return Vector3D(0, 0, 0);
|
||||
}
|
||||
|
||||
// Direction: from parent origin to node center
|
||||
Point3D node_center = node_world_center(node, parent_tf);
|
||||
Point3D parent_origin = parent_tf * Point3D(0, 0, 0);
|
||||
|
||||
Vector3D dir = node_center - parent_origin;
|
||||
double len = dir.norm();
|
||||
|
||||
if (len < 1e-9) {
|
||||
// Node is at parent origin — push along +Y instead
|
||||
return Vector3D(0, factor, 0);
|
||||
}
|
||||
|
||||
// Explode outward: push the node further away from the parent
|
||||
// Add factor * the original offset distance
|
||||
return dir.normalized() * factor * std::max(1.0, len);
|
||||
}
|
||||
|
||||
ExplodeResult explode_view(Assembly& assembly, const ExplodeConfig& config) {
|
||||
ExplodeResult result;
|
||||
|
||||
// Collect flat parts
|
||||
std::vector<FlatPart> parts;
|
||||
collect_flat_parts(assembly.root, Transform3D::Identity(), 0, parts);
|
||||
|
||||
// Compute original bounds
|
||||
result.original_bounds = assembly_bounds(assembly.root, Transform3D::Identity());
|
||||
|
||||
// Compute global explosion direction
|
||||
Vector3D global_dir = config.direction;
|
||||
if (global_dir.norm() < 1e-9) {
|
||||
// Auto direction: use the principal axis of the assembly bounds
|
||||
Vector3D ext = result.original_bounds.extent();
|
||||
if (ext.x() >= ext.y() && ext.x() >= ext.z()) {
|
||||
global_dir = Vector3D(1, 0, 0);
|
||||
} else if (ext.y() >= ext.z()) {
|
||||
global_dir = Vector3D(0, 1, 0);
|
||||
} else {
|
||||
global_dir = Vector3D(0, 0, 1);
|
||||
}
|
||||
}
|
||||
global_dir.normalize();
|
||||
|
||||
// Compute explosion displacement for each part
|
||||
// Displacement is proportional to depth and distance from root
|
||||
for (auto& part : parts) {
|
||||
ExplodeStep step;
|
||||
step.part_name = part.name;
|
||||
step.original_transform = part.node->local_transform;
|
||||
|
||||
// Base displacement: along global direction, scaled by depth
|
||||
Vector3D disp = config.use_constraint_dirs
|
||||
? compute_explode_displacement(*part.node, Transform3D::Identity(), config.factor)
|
||||
: global_dir * config.factor * (part.depth + 1) * 2.0;
|
||||
|
||||
step.displacement = disp;
|
||||
|
||||
// Apply explosion: translate the node's local transform
|
||||
Transform3D T = core::translate(disp);
|
||||
part.node->local_transform = T * part.node->local_transform;
|
||||
step.exploded_transform = part.node->local_transform;
|
||||
|
||||
result.steps.push_back(step);
|
||||
}
|
||||
|
||||
// Compute exploded bounds
|
||||
result.exploded_bounds = assembly_bounds(assembly.root, Transform3D::Identity());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void un_explode(Assembly& assembly, const ExplodeResult& result) {
|
||||
// Build a name → original transform map
|
||||
std::map<std::string, size_t> name_to_index;
|
||||
std::vector<FlatPart> parts;
|
||||
collect_flat_parts(assembly.root, Transform3D::Identity(), 0, parts);
|
||||
|
||||
for (size_t i = 0; i < parts.size(); ++i) {
|
||||
name_to_index[parts[i].name] = i;
|
||||
}
|
||||
|
||||
// Restore original transforms from explosion steps
|
||||
for (const auto& step : result.steps) {
|
||||
auto it = name_to_index.find(step.part_name);
|
||||
if (it != name_to_index.end()) {
|
||||
parts[it->second].node->local_transform = step.original_transform;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double ExplodeResult::max_displacement() const {
|
||||
double max_d = 0.0;
|
||||
for (const auto& s : steps) {
|
||||
double d = s.displacement.norm();
|
||||
if (d > max_d) max_d = d;
|
||||
}
|
||||
return max_d;
|
||||
}
|
||||
|
||||
} // namespace vde::brep
|
||||
Reference in New Issue
Block a user