use phyz_math::{Vec3, quat_exp, quat_log};
use phyz_model::{JointType, Model, State};
pub fn integrate_configuration(model: &Model, q: &mut [f64], v: &[f64], dt: f64) {
for (jidx, joint) in model.joints.iter().enumerate() {
let q_off = model.q_offsets[jidx];
let v_off = model.v_offsets[jidx];
match joint.joint_type {
JointType::Fixed => {}
JointType::Revolute | JointType::Hinge | JointType::Prismatic | JointType::Slide => {
q[q_off] += dt * v[v_off];
}
JointType::Spherical | JointType::Ball => {
let omega = Vec3::new(v[v_off], v[v_off + 1], v[v_off + 2]);
let current = quat_exp(&Vec3::new(q[q_off], q[q_off + 1], q[q_off + 2]));
let next = current.mul(&quat_exp(&(omega * dt))).normalize();
let log = quat_log(&next);
q[q_off] = log.x;
q[q_off + 1] = log.y;
q[q_off + 2] = log.z;
}
JointType::Free => {
let omega = Vec3::new(v[v_off], v[v_off + 1], v[v_off + 2]);
let lin = Vec3::new(v[v_off + 3], v[v_off + 4], v[v_off + 5]);
let current = quat_exp(&Vec3::new(q[q_off], q[q_off + 1], q[q_off + 2]));
let next = current.mul(&quat_exp(&(omega * dt))).normalize();
let world_lin = next.rotate(lin);
q[q_off + 3] += dt * world_lin.x;
q[q_off + 4] += dt * world_lin.y;
q[q_off + 5] += dt * world_lin.z;
let log = quat_log(&next);
q[q_off] = log.x;
q[q_off + 1] = log.y;
q[q_off + 2] = log.z;
}
}
}
}
pub fn strip_free_joint_coriolis(model: &Model, v: &[f64], qdd: &mut [f64]) {
for (jidx, joint) in model.joints.iter().enumerate() {
if joint.joint_type != JointType::Free {
continue;
}
let off = model.v_offsets[jidx];
let omega = Vec3::new(v[off], v[off + 1], v[off + 2]);
let lin = Vec3::new(v[off + 3], v[off + 4], v[off + 5]);
let c = omega.cross(lin);
qdd[off + 3] += c.x;
qdd[off + 4] += c.y;
qdd[off + 5] += c.z;
}
}
pub fn rotate_free_joint_velocities(model: &Model, v_before: &[f64], v: &mut [f64], dt: f64) {
for (jidx, joint) in model.joints.iter().enumerate() {
if joint.joint_type != JointType::Free {
continue;
}
let off = model.v_offsets[jidx];
let omega = Vec3::new(v_before[off], v_before[off + 1], v_before[off + 2]);
let theta = omega.norm() * dt;
if theta < 1e-12 || !theta.is_finite() {
continue;
}
let lin = Vec3::new(v[off + 3], v[off + 4], v[off + 5]);
let turned = quat_exp(&(omega * -dt)).rotate(lin);
v[off + 3] = turned.x;
v[off + 4] = turned.y;
v[off + 5] = turned.z;
}
}
pub fn semi_implicit_euler(model: &Model, state: &mut State, qdd: &[f64], dt: f64) {
let mut qdd = qdd[..model.nv].to_vec();
let v_before = state.v.clone();
strip_free_joint_coriolis(model, v_before.as_slice(), &mut qdd);
for (i, a) in qdd.iter().enumerate() {
state.v[i] += dt * a;
}
rotate_free_joint_velocities(model, v_before.as_slice(), state.v.as_mut_slice(), dt);
let v = state.v.clone();
integrate_configuration(model, state.q.as_mut_slice(), v.as_slice(), dt);
state.time += dt;
}
#[cfg(test)]
mod tests {
use super::*;
use phyz_math::{SpatialInertia, SpatialTransform};
use phyz_model::ModelBuilder;
#[test]
fn hinge_integrates_scalar() {
let model = ModelBuilder::new()
.dt(0.01)
.add_revolute_body(
"l",
-1,
SpatialTransform::identity(),
SpatialInertia::point_mass(1.0, Vec3::new(0.0, 0.0, -0.5)),
)
.build();
let mut s = model.default_state();
semi_implicit_euler(&model, &mut s, &[2.0], 0.1);
assert!((s.v[0] - 0.2).abs() < 1e-12);
assert!((s.q[0] - 0.02).abs() < 1e-12);
assert!((s.time - 0.1).abs() < 1e-12);
}
}