Skip to main content

box3d_rust/joint/
api.rs

1//! Generic `b3Joint_*` public API from joint.c (tuning, frames, userdata,
2//! wake, constraint force/torque, separation).
3//!
4//! SPDX-FileCopyrightText: 2025 Erin Catto
5//! SPDX-License-Identifier: MIT
6
7use super::{
8    get_distance_joint_force, get_joint_full_id, get_joint_sim, get_joint_sim_ref,
9    get_motor_joint_force, get_motor_joint_torque, get_parallel_joint_torque,
10    get_prismatic_joint_force, get_prismatic_joint_torque, get_revolute_joint_force,
11    get_revolute_joint_torque, get_spherical_joint_force, get_spherical_joint_torque,
12    get_weld_joint_force, get_weld_joint_torque, get_wheel_joint_force, get_wheel_joint_torque,
13    JointSim, JointType,
14};
15use crate::body::{get_body_transform, wake_body};
16use crate::core::NULL_INDEX;
17use crate::id::{JointId, WorldId};
18use crate::math_functions::{
19    abs_float, dot, get_quat_angle, get_swing_angle, get_twist_angle, inv_mul_quat, is_valid_float,
20    is_valid_transform, length, max_float, perp, rotate_vector, sub_pos, transform_world_point,
21    Transform, Vec3, VEC3_AXIS_X, VEC3_ZERO,
22};
23use crate::world::World;
24
25/// (b3Joint_SetConstraintTuning)
26pub fn joint_set_constraint_tuning(
27    world: &mut World,
28    joint_id: JointId,
29    hertz: f32,
30    damping_ratio: f32,
31) {
32    crate::recording::with_recording(world, |rec| {
33        rec.write_joint_set_constraint_tuning(joint_id, hertz, damping_ratio);
34    });
35    debug_assert!(is_valid_float(hertz) && hertz >= 0.0);
36    debug_assert!(is_valid_float(damping_ratio) && damping_ratio >= 0.0);
37
38    let id = get_joint_full_id(world, joint_id);
39    let base = get_joint_sim(world, id);
40    base.constraint_hertz = hertz;
41    base.constraint_damping_ratio = damping_ratio;
42}
43
44/// (b3Joint_GetConstraintTuning)
45pub fn joint_get_constraint_tuning(world: &World, joint_id: JointId) -> (f32, f32) {
46    let id = get_joint_full_id(world, joint_id);
47    let base = get_joint_sim_ref(world, id);
48    (base.constraint_hertz, base.constraint_damping_ratio)
49}
50
51/// (b3Joint_SetForceThreshold)
52pub fn joint_set_force_threshold(world: &mut World, joint_id: JointId, threshold: f32) {
53    crate::recording::with_recording(world, |rec| {
54        rec.write_joint_set_force_threshold(joint_id, threshold);
55    });
56    debug_assert!(is_valid_float(threshold) && threshold >= 0.0);
57
58    let id = get_joint_full_id(world, joint_id);
59    get_joint_sim(world, id).force_threshold = threshold;
60}
61
62/// (b3Joint_GetForceThreshold)
63pub fn joint_get_force_threshold(world: &World, joint_id: JointId) -> f32 {
64    let id = get_joint_full_id(world, joint_id);
65    get_joint_sim_ref(world, id).force_threshold
66}
67
68/// (b3Joint_SetTorqueThreshold)
69pub fn joint_set_torque_threshold(world: &mut World, joint_id: JointId, threshold: f32) {
70    crate::recording::with_recording(world, |rec| {
71        rec.write_joint_set_torque_threshold(joint_id, threshold);
72    });
73    debug_assert!(is_valid_float(threshold) && threshold >= 0.0);
74
75    let id = get_joint_full_id(world, joint_id);
76    get_joint_sim(world, id).torque_threshold = threshold;
77}
78
79/// (b3Joint_GetTorqueThreshold)
80pub fn joint_get_torque_threshold(world: &World, joint_id: JointId) -> f32 {
81    let id = get_joint_full_id(world, joint_id);
82    get_joint_sim_ref(world, id).torque_threshold
83}
84
85/// (b3Joint_GetWorld)
86pub fn joint_get_world(world: &World, joint_id: JointId) -> WorldId {
87    let _ = get_joint_full_id(world, joint_id);
88    WorldId {
89        index1: joint_id.world0.wrapping_add(1),
90        generation: world.generation,
91    }
92}
93
94/// (b3Joint_SetLocalFrameA)
95pub fn joint_set_local_frame_a(world: &mut World, joint_id: JointId, local_frame: Transform) {
96    crate::recording::with_recording(world, |rec| {
97        rec.write_joint_set_local_frame_a(joint_id, local_frame);
98    });
99    debug_assert!(is_valid_transform(local_frame));
100
101    let id = get_joint_full_id(world, joint_id);
102    get_joint_sim(world, id).local_frame_a = local_frame;
103}
104
105/// (b3Joint_GetLocalFrameA)
106pub fn joint_get_local_frame_a(world: &World, joint_id: JointId) -> Transform {
107    let id = get_joint_full_id(world, joint_id);
108    get_joint_sim_ref(world, id).local_frame_a
109}
110
111/// (b3Joint_SetLocalFrameB)
112pub fn joint_set_local_frame_b(world: &mut World, joint_id: JointId, local_frame: Transform) {
113    crate::recording::with_recording(world, |rec| {
114        rec.write_joint_set_local_frame_b(joint_id, local_frame);
115    });
116    debug_assert!(is_valid_transform(local_frame));
117
118    let id = get_joint_full_id(world, joint_id);
119    get_joint_sim(world, id).local_frame_b = local_frame;
120}
121
122/// (b3Joint_GetLocalFrameB)
123pub fn joint_get_local_frame_b(world: &World, joint_id: JointId) -> Transform {
124    let id = get_joint_full_id(world, joint_id);
125    get_joint_sim_ref(world, id).local_frame_b
126}
127
128/// (b3Joint_SetUserData) — Rust stores `u64` instead of `void*`.
129pub fn joint_set_user_data(world: &mut World, joint_id: JointId, user_data: u64) {
130    let id = get_joint_full_id(world, joint_id);
131    world.joints[id as usize].user_data = user_data;
132}
133
134/// (b3Joint_GetUserData)
135pub fn joint_get_user_data(world: &World, joint_id: JointId) -> u64 {
136    let id = get_joint_full_id(world, joint_id);
137    world.joints[id as usize].user_data
138}
139
140/// (b3Joint_WakeBodies)
141pub fn joint_wake_bodies(world: &mut World, joint_id: JointId) {
142    crate::recording::with_recording(world, |rec| {
143        rec.write_joint_wake_bodies(joint_id);
144    });
145    debug_assert!(!world.locked);
146    if world.locked {
147        return;
148    }
149
150    world.locked = true;
151
152    let id = get_joint_full_id(world, joint_id);
153    let body_id_a = world.joints[id as usize].edges[0].body_id;
154    let body_id_b = world.joints[id as usize].edges[1].body_id;
155
156    wake_body(world, body_id_a);
157    wake_body(world, body_id_b);
158
159    world.locked = false;
160}
161
162/// Copy of the joint sim so force helpers can also borrow `&World`.
163fn joint_sim_copy(world: &World, joint_id: i32) -> JointSim {
164    *get_joint_sim_ref(world, joint_id)
165}
166
167/// (b3GetJointConstraintForce / b3Joint_GetConstraintForce)
168pub fn joint_get_constraint_force(world: &World, joint_id: JointId) -> Vec3 {
169    let id = get_joint_full_id(world, joint_id);
170    let type_ = world.joints[id as usize].type_;
171    let base = joint_sim_copy(world, id);
172
173    match type_ {
174        JointType::Parallel => VEC3_ZERO,
175        JointType::Distance => get_distance_joint_force(world, &base),
176        JointType::Filter => VEC3_ZERO,
177        JointType::Motor => get_motor_joint_force(world, &base),
178        JointType::Prismatic => get_prismatic_joint_force(world, &base),
179        JointType::Revolute => get_revolute_joint_force(world, &base),
180        JointType::Spherical => get_spherical_joint_force(world, &base),
181        JointType::Weld => get_weld_joint_force(world, &base),
182        JointType::Wheel => get_wheel_joint_force(world, &base),
183    }
184}
185
186/// (b3GetJointConstraintTorque / b3Joint_GetConstraintTorque)
187pub fn joint_get_constraint_torque(world: &mut World, joint_id: JointId) -> Vec3 {
188    let id = get_joint_full_id(world, joint_id);
189    let type_ = world.joints[id as usize].type_;
190
191    match type_ {
192        JointType::Parallel => {
193            let inv_h = world.inv_h;
194            let mut base = joint_sim_copy(world, id);
195            get_parallel_joint_torque(inv_h, &mut base)
196        }
197        JointType::Distance | JointType::Filter => VEC3_ZERO,
198        JointType::Motor => {
199            let base = joint_sim_copy(world, id);
200            get_motor_joint_torque(world, &base)
201        }
202        JointType::Prismatic => {
203            let base = joint_sim_copy(world, id);
204            get_prismatic_joint_torque(world, &base)
205        }
206        JointType::Revolute => {
207            // C recomputes perp axes as a side effect — write back.
208            with_joint_sim_mut(world, id, |world, base| {
209                get_revolute_joint_torque(world, base)
210            })
211        }
212        JointType::Spherical => {
213            let base = joint_sim_copy(world, id);
214            get_spherical_joint_torque(world, &base)
215        }
216        JointType::Weld => {
217            let base = joint_sim_copy(world, id);
218            get_weld_joint_torque(world, &base)
219        }
220        JointType::Wheel => {
221            let base = joint_sim_copy(world, id);
222            get_wheel_joint_torque(world, &base)
223        }
224    }
225}
226
227fn with_joint_sim_mut<R>(
228    world: &mut World,
229    joint_id: i32,
230    f: impl FnOnce(&World, &mut JointSim) -> R,
231) -> R {
232    let set_index = world.joints[joint_id as usize].set_index;
233    let color_index = world.joints[joint_id as usize].color_index;
234    let local_index = world.joints[joint_id as usize].local_index as usize;
235
236    if color_index != NULL_INDEX {
237        let mut sim = std::mem::take(
238            &mut world.constraint_graph.colors[color_index as usize].joint_sims[local_index],
239        );
240        let result = f(world, &mut sim);
241        world.constraint_graph.colors[color_index as usize].joint_sims[local_index] = sim;
242        result
243    } else {
244        let mut sim =
245            std::mem::take(&mut world.solver_sets[set_index as usize].joint_sims[local_index]);
246        let result = f(world, &mut sim);
247        world.solver_sets[set_index as usize].joint_sims[local_index] = sim;
248        result
249    }
250}
251
252/// (b3Joint_GetLinearSeparation)
253pub fn joint_get_linear_separation(world: &World, joint_id: JointId) -> f32 {
254    let id = get_joint_full_id(world, joint_id);
255    let type_ = world.joints[id as usize].type_;
256    let body_id_a = world.joints[id as usize].edges[0].body_id;
257    let body_id_b = world.joints[id as usize].edges[1].body_id;
258    let base = joint_sim_copy(world, id);
259
260    let xf_a = get_body_transform(world, body_id_a);
261    let xf_b = get_body_transform(world, body_id_b);
262
263    let p_a = transform_world_point(xf_a, base.local_frame_a.p);
264    let p_b = transform_world_point(xf_b, base.local_frame_b.p);
265    let dp = sub_pos(p_b, p_a);
266
267    match type_ {
268        JointType::Parallel | JointType::Motor | JointType::Filter => 0.0,
269        JointType::Distance => {
270            let distance_joint = base.distance();
271            let len = length(dp);
272            if distance_joint.enable_spring {
273                if distance_joint.enable_limit {
274                    if len < distance_joint.min_length {
275                        return distance_joint.min_length - len;
276                    }
277                    if len > distance_joint.max_length {
278                        return len - distance_joint.max_length;
279                    }
280                    return 0.0;
281                }
282                return 0.0;
283            }
284            abs_float(len - distance_joint.length)
285        }
286        JointType::Prismatic => {
287            let prismatic = base.prismatic();
288            let axis_a = rotate_vector(xf_a.q, VEC3_AXIS_X);
289            let perp_a = perp(axis_a);
290            let perpendicular_separation = abs_float(dot(perp_a, dp));
291            let mut limit_separation = 0.0f32;
292
293            if prismatic.enable_limit {
294                let translation = dot(axis_a, dp);
295                if translation < prismatic.lower_translation {
296                    limit_separation = prismatic.lower_translation - translation;
297                }
298                if prismatic.upper_translation < translation {
299                    limit_separation = translation - prismatic.upper_translation;
300                }
301            }
302
303            (perpendicular_separation * perpendicular_separation
304                + limit_separation * limit_separation)
305                .sqrt()
306        }
307        JointType::Revolute | JointType::Spherical => length(dp),
308        JointType::Weld => {
309            if base.weld().linear_hertz == 0.0 {
310                length(dp)
311            } else {
312                0.0
313            }
314        }
315        JointType::Wheel => {
316            let wheel = base.wheel();
317            let axis_a = rotate_vector(xf_a.q, VEC3_AXIS_X);
318            let perp_a = perp(axis_a);
319            let perpendicular_separation = abs_float(dot(perp_a, dp));
320            let mut limit_separation = 0.0f32;
321
322            if wheel.enable_suspension_limit {
323                let translation = dot(axis_a, dp);
324                if translation < wheel.lower_suspension_limit {
325                    limit_separation = wheel.lower_suspension_limit - translation;
326                }
327                if wheel.upper_suspension_limit < translation {
328                    limit_separation = translation - wheel.upper_suspension_limit;
329                }
330            }
331
332            (perpendicular_separation * perpendicular_separation
333                + limit_separation * limit_separation)
334                .sqrt()
335        }
336    }
337}
338
339/// (b3Joint_GetAngularSeparation)
340pub fn joint_get_angular_separation(world: &World, joint_id: JointId) -> f32 {
341    let id = get_joint_full_id(world, joint_id);
342    let type_ = world.joints[id as usize].type_;
343    let body_id_a = world.joints[id as usize].edges[0].body_id;
344    let body_id_b = world.joints[id as usize].edges[1].body_id;
345    let base = joint_sim_copy(world, id);
346
347    let xf_a = get_body_transform(world, body_id_a);
348    let xf_b = get_body_transform(world, body_id_b);
349
350    let mut rel_q = inv_mul_quat(xf_a.q, xf_b.q);
351
352    match type_ {
353        JointType::Parallel => {
354            // Remove hinge angle
355            rel_q.v.z = 0.0;
356            get_quat_angle(rel_q)
357        }
358        JointType::Distance | JointType::Motor | JointType::Filter => 0.0,
359        JointType::Prismatic => get_quat_angle(rel_q),
360        JointType::Revolute => {
361            let revolute = base.revolute();
362            if revolute.enable_limit {
363                let angle = get_twist_angle(rel_q);
364                if angle < revolute.lower_angle || revolute.upper_angle < angle {
365                    return get_quat_angle(rel_q);
366                }
367            }
368            // Remove hinge angle
369            rel_q.v.z = 0.0;
370            get_quat_angle(rel_q)
371        }
372        JointType::Spherical => {
373            let spherical = base.spherical();
374            let mut sum = 0.0f32;
375            if spherical.enable_cone_limit {
376                let swing_angle = get_swing_angle(rel_q);
377                sum += max_float(0.0, swing_angle - spherical.cone_angle);
378            }
379            if spherical.enable_twist_limit {
380                let twist_angle = get_twist_angle(rel_q);
381                sum += max_float(0.0, spherical.lower_twist_angle - twist_angle);
382                sum += max_float(0.0, twist_angle - spherical.upper_twist_angle);
383            }
384            sum
385        }
386        JointType::Weld => {
387            if base.weld().angular_hertz == 0.0 {
388                get_quat_angle(rel_q)
389            } else {
390                0.0
391            }
392        }
393        JointType::Wheel => {
394            // todo in C — asserts; skip for wheel in callers
395            debug_assert!(false, "wheel angular separation unimplemented in C");
396            0.0
397        }
398    }
399}