Skip to main content

box3d_rust/joint/
solve.rs

1//! Joint solver stage dispatch: prepare / warm-start / solve / reaction.
2//! Port of b3PrepareJoint, b3WarmStartJoint, b3SolveJoint, b3GetJointReaction
3//! from box3d-cpp-reference/src/joint.c.
4//!
5//! SPDX-FileCopyrightText: 2025 Erin Catto
6//! SPDX-License-Identifier: MIT
7
8use super::{
9    prepare_distance_joint, prepare_motor_joint, prepare_parallel_joint, prepare_prismatic_joint,
10    prepare_revolute_joint, prepare_spherical_joint, prepare_weld_joint, prepare_wheel_joint,
11    solve_distance_joint, solve_motor_joint, solve_parallel_joint, solve_prismatic_joint,
12    solve_revolute_joint, solve_spherical_joint, solve_weld_joint, solve_wheel_joint,
13    warm_start_distance_joint, warm_start_motor_joint, warm_start_parallel_joint,
14    warm_start_prismatic_joint, warm_start_revolute_joint, warm_start_spherical_joint,
15    warm_start_weld_joint, warm_start_wheel_joint, JointSim, JointType,
16};
17use crate::body::{get_body_transform, BodyState};
18use crate::math_functions::{
19    abs_float, add, cross, length, min_float, mul_add, mul_quat, normalize, rotate_vector, Vec3,
20    VEC3_AXIS_Z,
21};
22use crate::solver::{make_soft, StepContext};
23use crate::world::World;
24
25/// Clamp constraint hertz and dispatch prepare. (b3PrepareJoint)
26pub fn prepare_joint(world: &World, base: &mut JointSim, context: &StepContext) {
27    // Clamp joint hertz based on the time step to reduce jitter.
28    let hertz = min_float(base.constraint_hertz, 0.25 * context.inv_h);
29    base.constraint_softness = make_soft(hertz, base.constraint_damping_ratio, context.h);
30
31    match base.type_ {
32        JointType::Parallel => prepare_parallel_joint(world, base, context),
33        JointType::Distance => prepare_distance_joint(world, base, context),
34        JointType::Filter => {}
35        JointType::Motor => prepare_motor_joint(world, base, context),
36        JointType::Prismatic => prepare_prismatic_joint(world, base, context),
37        JointType::Revolute => prepare_revolute_joint(world, base, context),
38        JointType::Spherical => prepare_spherical_joint(world, base, context),
39        JointType::Weld => prepare_weld_joint(world, base, context),
40        JointType::Wheel => prepare_wheel_joint(world, base, context),
41    }
42}
43
44/// (b3WarmStartJoint)
45pub fn warm_start_joint(base: &mut JointSim, states: &mut [BodyState]) {
46    match base.type_ {
47        JointType::Parallel => warm_start_parallel_joint(base, states),
48        JointType::Distance => warm_start_distance_joint(base, states),
49        JointType::Filter => {}
50        JointType::Motor => warm_start_motor_joint(base, states),
51        JointType::Prismatic => warm_start_prismatic_joint(base, states),
52        JointType::Revolute => warm_start_revolute_joint(base, states),
53        JointType::Spherical => warm_start_spherical_joint(base, states),
54        JointType::Weld => warm_start_weld_joint(base, states),
55        JointType::Wheel => warm_start_wheel_joint(base, states),
56    }
57}
58
59/// (b3SolveJoint)
60///
61/// Motor and parallel ignore `use_bias` like C (`B3_UNUSED(useBias)` at the
62/// top of b3SolveJoint; those callees have no useBias parameter).
63pub fn solve_joint(
64    base: &mut JointSim,
65    context: &StepContext,
66    states: &mut [BodyState],
67    use_bias: bool,
68) {
69    let _ = use_bias;
70
71    match base.type_ {
72        JointType::Parallel => solve_parallel_joint(base, context, states),
73        JointType::Distance => solve_distance_joint(base, context, states, use_bias),
74        JointType::Filter => {}
75        JointType::Motor => solve_motor_joint(base, context, states),
76        JointType::Prismatic => solve_prismatic_joint(base, context, states, use_bias),
77        JointType::Revolute => solve_revolute_joint(base, context, states, use_bias),
78        JointType::Spherical => solve_spherical_joint(base, context, states, use_bias),
79        JointType::Weld => solve_weld_joint(base, context, states, use_bias),
80        JointType::Wheel => solve_wheel_joint(base, context, states, use_bias),
81    }
82}
83
84/// Scalar force/torque magnitudes from accumulated impulses. (b3GetJointReaction)
85pub fn get_joint_reaction(world: &World, sim: &JointSim, inv_time_step: f32) -> (f32, f32) {
86    let mut linear_impulse = 0.0f32;
87    let mut angular_impulse = 0.0f32;
88
89    match sim.type_ {
90        JointType::Parallel => {
91            let joint = sim.parallel();
92            let impulse = Vec3 {
93                x: joint.perp_impulse.x,
94                y: joint.perp_impulse.y,
95                z: 0.0,
96            };
97            angular_impulse = length(impulse);
98        }
99        JointType::Distance => {
100            let joint = sim.distance();
101            linear_impulse = abs_float(
102                joint.impulse + joint.lower_impulse - joint.upper_impulse + joint.motor_impulse,
103            );
104        }
105        JointType::Motor => {
106            let joint = sim.motor();
107            linear_impulse = length(add(
108                joint.linear_velocity_impulse,
109                joint.linear_spring_impulse,
110            ));
111            angular_impulse = length(add(
112                joint.angular_velocity_impulse,
113                joint.angular_spring_impulse,
114            ));
115        }
116        JointType::Prismatic => {
117            let joint = sim.prismatic();
118            let impulse = Vec3 {
119                x: joint.motor_impulse + joint.lower_impulse - joint.upper_impulse,
120                y: joint.perp_impulse.x,
121                z: joint.perp_impulse.y,
122            };
123            linear_impulse = length(impulse);
124            angular_impulse = length(joint.angular_impulse);
125        }
126        JointType::Revolute => {
127            let joint = sim.revolute();
128            linear_impulse = length(joint.linear_impulse);
129            let impulse = Vec3 {
130                x: joint.perp_impulse.x,
131                y: joint.perp_impulse.y,
132                z: joint.motor_impulse + joint.lower_impulse - joint.upper_impulse,
133            };
134            angular_impulse = length(impulse);
135        }
136        JointType::Spherical => {
137            let joint = sim.spherical();
138            linear_impulse = length(joint.linear_impulse);
139
140            let xf_a = get_body_transform(world, sim.body_id_a);
141            let xf_b = get_body_transform(world, sim.body_id_b);
142            let q_a = mul_quat(xf_a.q, sim.local_frame_a.q);
143            let q_b = mul_quat(xf_b.q, sim.local_frame_b.q);
144
145            // Cone axis is the z-axis of body A.
146            let cone_axis = rotate_vector(q_a, VEC3_AXIS_Z);
147            let twist_axis = rotate_vector(q_b, VEC3_AXIS_Z);
148            let swing_axis = normalize(cross(cone_axis, twist_axis));
149
150            let mut impulse = add(joint.spring_impulse, joint.motor_impulse);
151            impulse = mul_add(
152                impulse,
153                joint.lower_twist_impulse - joint.upper_twist_impulse,
154                twist_axis,
155            );
156            impulse = mul_add(impulse, joint.swing_impulse, swing_axis);
157
158            angular_impulse = length(impulse);
159        }
160        JointType::Weld => {
161            let joint = sim.weld();
162            linear_impulse = length(joint.linear_impulse);
163            angular_impulse = length(joint.angular_impulse);
164        }
165        JointType::Wheel => {
166            // todo probably wrong (matches C comment)
167            let joint = sim.wheel();
168            let perp_impulse = joint.linear_impulse;
169            let axial_impulse = joint.suspension_spring_impulse + joint.lower_suspension_impulse
170                - joint.upper_suspension_impulse;
171            linear_impulse = (perp_impulse.x * perp_impulse.x
172                + perp_impulse.y * perp_impulse.y
173                + axial_impulse * axial_impulse)
174                .sqrt();
175            angular_impulse = abs_float(joint.spin_impulse);
176        }
177        JointType::Filter => {}
178    }
179
180    (
181        linear_impulse * inv_time_step,
182        angular_impulse * inv_time_step,
183    )
184}