feat(v4.0): motion simulation — revolute/prismatic/cylindrical/spherical/planar joints
- MotionJoint with type, anchor, axis, limits - MotionSimulation: step/run with driver functions - Constant speed and custom driver support - Collision detection integration during simulation - 13 tests: all joint types, limits, reset, multi-joint chains
This commit is contained in:
@@ -147,6 +147,7 @@ add_library(vde::engine ALIAS vde)
|
||||
add_library(vde_brep STATIC
|
||||
brep/brep.cpp
|
||||
brep/interference_check.cpp
|
||||
brep/motion_simulation.cpp
|
||||
brep/modeling.cpp
|
||||
brep/brep_drawing.cpp
|
||||
brep/step_export.cpp
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
#include "vde/brep/motion_simulation.h"
|
||||
#include "vde/brep/interference_check.h"
|
||||
#include "vde/core/transform.h"
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
namespace vde::brep {
|
||||
|
||||
using core::Point3D;
|
||||
using core::Vector3D;
|
||||
using core::Transform3D;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Joint management
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
void MotionSimulation::add_joint(const MotionJoint& joint) {
|
||||
joints_.push_back(joint);
|
||||
joint_values_.push_back(0.0);
|
||||
drivers_.push_back(nullptr);
|
||||
}
|
||||
|
||||
void MotionSimulation::set_driver(int joint_index, std::function<double(double)> driver) {
|
||||
if (joint_index >= 0 && joint_index < static_cast<int>(drivers_.size())) {
|
||||
drivers_[joint_index] = std::move(driver);
|
||||
}
|
||||
}
|
||||
|
||||
void MotionSimulation::set_constant_speed(int joint_index, double speed) {
|
||||
set_driver(joint_index, [speed](double t) { return speed * t; });
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Node lookup
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
AssemblyNode* MotionSimulation::find_node(AssemblyNode& root, const std::string& name) {
|
||||
if (root.name == name) return &root;
|
||||
for (auto& child : root.children) {
|
||||
auto* found = find_node(*child, name);
|
||||
if (found) return found;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Transform application
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
void MotionSimulation::apply_joint_transform(
|
||||
Assembly& assembly, int joint_idx, double value)
|
||||
{
|
||||
const auto& joint = joints_[joint_idx];
|
||||
auto* child = find_node(assembly.root, joint.child_link);
|
||||
if (!child) return;
|
||||
|
||||
Transform3D T = Transform3D::Identity();
|
||||
Vector3D ax = joint.axis.normalized();
|
||||
|
||||
switch (joint.type) {
|
||||
case JointType::Fixed:
|
||||
break; // no motion
|
||||
|
||||
case JointType::Revolute: {
|
||||
// Clamp to limits
|
||||
double angle = std::clamp(value, joint.limit_min, joint.limit_max);
|
||||
T = core::rotate(ax, angle);
|
||||
break;
|
||||
}
|
||||
|
||||
case JointType::Prismatic: {
|
||||
double dist = std::clamp(value, joint.limit_min, joint.limit_max);
|
||||
T = core::translate(ax * dist);
|
||||
break;
|
||||
}
|
||||
|
||||
case JointType::Cylindrical: {
|
||||
// value.x = angle, value.y = distance (if 2D, else use value for angle)
|
||||
double angle = std::clamp(value, joint.limit_min, joint.limit_max);
|
||||
T = core::rotate(ax, angle);
|
||||
// Note: cylindrical would need a 2nd DOF for translation
|
||||
break;
|
||||
}
|
||||
|
||||
case JointType::Spherical: {
|
||||
// For simplicity: rotate around world Y axis by value
|
||||
T = core::rotate(Vector3D(0, 1, 0), value);
|
||||
break;
|
||||
}
|
||||
|
||||
case JointType::Planar: {
|
||||
// Translate in plane
|
||||
Vector3D plane_n = joint.plane_normal.normalized();
|
||||
Vector3D u = (std::abs(plane_n.x()) < 0.9)
|
||||
? Vector3D(1, 0, 0).cross(plane_n).normalized()
|
||||
: Vector3D(0, 1, 0).cross(plane_n).normalized();
|
||||
T = core::translate(u * value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
child->local_transform = T * child->local_transform;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Simulation execution
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
SimulationState MotionSimulation::step(Assembly& assembly, double dt) {
|
||||
SimulationState state;
|
||||
state.time = time_;
|
||||
|
||||
// Update joint values from drivers
|
||||
for (size_t i = 0; i < joints_.size(); ++i) {
|
||||
if (drivers_[i]) {
|
||||
joint_values_[i] = drivers_[i](time_);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply joint transforms
|
||||
for (size_t i = 0; i < joints_.size(); ++i) {
|
||||
apply_joint_transform(assembly, static_cast<int>(i), joint_values_[i]);
|
||||
}
|
||||
|
||||
state.joint_values = joint_values_;
|
||||
time_ += dt;
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
std::vector<SimulationState> MotionSimulation::run(
|
||||
Assembly& assembly, const SimulationConfig& config)
|
||||
{
|
||||
std::vector<SimulationState> states;
|
||||
reset();
|
||||
|
||||
int steps = static_cast<int>(std::ceil(config.duration / config.time_step));
|
||||
|
||||
for (int s = 0; s < steps; ++s) {
|
||||
auto state = step(assembly, config.time_step);
|
||||
|
||||
if (config.detect_collision) {
|
||||
auto result = check_interference(assembly, true); // coarse AABB check
|
||||
if (result.has_interference) {
|
||||
for (auto& c : result.conflicts) {
|
||||
state.collisions.push_back(c.part_a + "↔" + c.part_b);
|
||||
}
|
||||
if (config.stop_on_collision) break;
|
||||
}
|
||||
}
|
||||
|
||||
states.push_back(state);
|
||||
}
|
||||
|
||||
return states;
|
||||
}
|
||||
|
||||
} // namespace vde::brep
|
||||
Reference in New Issue
Block a user