feat(v4.0): explode view — assembly part decomposition
CI / Build & Test (push) Failing after 36s
CI / Release Build (push) Failing after 47s
Build & Test / build-and-test (push) Has been cancelled
Build & Test / python-bindings (push) Has been cancelled

- 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:
茂之钳
2026-07-25 03:18:43 +00:00
parent d61c8d8a7b
commit a2b34733bf
8 changed files with 519 additions and 0 deletions
+1
View File
@@ -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
+202
View File
@@ -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