Skip to main content

box2d_rust/joint/
api.rs

1// Joint public API from joint.c (b2Joint_*). The C resolves the world from
2// the id via the global registry; the Rust port takes `world` explicitly.
3// b2Joint_GetWorld is not ported (there is no world registry).
4//
5// SPDX-FileCopyrightText: 2023 Erin Catto
6// SPDX-License-Identifier: MIT
7//
8// bring-up: exercised by the world API slice and tests.
9
10use super::*;
11use crate::body::{make_body_id, wake_body};
12use crate::core::NULL_INDEX;
13use crate::id::{BodyId, JointId};
14use crate::math_functions::{
15    abs_float, dot, is_valid_float, is_valid_transform, left_perp, length, relative_angle,
16    rotate_vector, sub, to_relative_transform, transform_point, Transform, Vec2,
17};
18use crate::world::World;
19
20/// Joint id validity. (b2Joint_IsValid — the world-registry check collapses
21/// to the index/generation check in the registry-less port)
22pub fn joint_is_valid(world: &World, id: JointId) -> bool {
23    let joint_index = id.index1 - 1;
24    if joint_index < 0 || world.joints.len() as i32 <= joint_index {
25        return false;
26    }
27
28    let joint = &world.joints[joint_index as usize];
29    if joint.joint_id == NULL_INDEX {
30        // joint is free
31        return false;
32    }
33
34    debug_assert!(joint.joint_id == joint_index);
35
36    id.generation == joint.generation
37}
38
39/// (b2Joint_GetType)
40pub fn joint_get_type(world: &World, joint_id: JointId) -> JointType {
41    let joint_index = get_joint_full_id(world, joint_id);
42    world.joints[joint_index as usize].type_
43}
44
45/// (b2Joint_GetBodyA)
46pub fn joint_get_body_a(world: &World, joint_id: JointId) -> BodyId {
47    let joint_index = get_joint_full_id(world, joint_id);
48    make_body_id(world, world.joints[joint_index as usize].edges[0].body_id)
49}
50
51/// (b2Joint_GetBodyB)
52pub fn joint_get_body_b(world: &World, joint_id: JointId) -> BodyId {
53    let joint_index = get_joint_full_id(world, joint_id);
54    make_body_id(world, world.joints[joint_index as usize].edges[1].body_id)
55}
56
57/// (b2Joint_SetLocalFrameA)
58pub fn joint_set_local_frame_a(world: &mut World, joint_id: JointId, local_frame: Transform) {
59    crate::recording::record_op(world, |rec, _| {
60        crate::recording::write_joint_xf(
61            rec,
62            crate::recording::OP_JOINT_SET_LOCAL_FRAME_A,
63            joint_id,
64            local_frame,
65        )
66    });
67    debug_assert!(is_valid_transform(local_frame));
68
69    let joint_index = get_joint_full_id(world, joint_id);
70    let joint_sim = get_joint_sim(world, joint_index);
71    joint_sim.local_frame_a = local_frame;
72}
73
74/// (b2Joint_GetLocalFrameA)
75pub fn joint_get_local_frame_a(world: &World, joint_id: JointId) -> Transform {
76    let joint_index = get_joint_full_id(world, joint_id);
77    get_joint_sim_ref(world, joint_index).local_frame_a
78}
79
80/// (b2Joint_SetLocalFrameB)
81pub fn joint_set_local_frame_b(world: &mut World, joint_id: JointId, local_frame: Transform) {
82    crate::recording::record_op(world, |rec, _| {
83        crate::recording::write_joint_xf(
84            rec,
85            crate::recording::OP_JOINT_SET_LOCAL_FRAME_B,
86            joint_id,
87            local_frame,
88        )
89    });
90    debug_assert!(is_valid_transform(local_frame));
91
92    let joint_index = get_joint_full_id(world, joint_id);
93    let joint_sim = get_joint_sim(world, joint_index);
94    joint_sim.local_frame_b = local_frame;
95}
96
97/// (b2Joint_GetLocalFrameB)
98pub fn joint_get_local_frame_b(world: &World, joint_id: JointId) -> Transform {
99    let joint_index = get_joint_full_id(world, joint_id);
100    get_joint_sim_ref(world, joint_index).local_frame_b
101}
102
103/// (b2Joint_SetCollideConnected)
104pub fn joint_set_collide_connected(world: &mut World, joint_id: JointId, should_collide: bool) {
105    crate::recording::record_op(world, |rec, _| {
106        crate::recording::write_joint_bool(
107            rec,
108            crate::recording::OP_JOINT_SET_COLLIDE_CONNECTED,
109            joint_id,
110            should_collide,
111        )
112    });
113    let joint_index = get_joint_full_id(world, joint_id);
114    if world.joints[joint_index as usize].collide_connected == should_collide {
115        return;
116    }
117
118    world.joints[joint_index as usize].collide_connected = should_collide;
119
120    let body_id_a = world.joints[joint_index as usize].edges[0].body_id;
121    let body_id_b = world.joints[joint_index as usize].edges[1].body_id;
122
123    if should_collide {
124        // need to tell the broad-phase to look for new pairs for one of the
125        // two bodies. Pick the one with the fewest shapes.
126        let shape_count_a = world.bodies[body_id_a as usize].shape_count;
127        let shape_count_b = world.bodies[body_id_b as usize].shape_count;
128
129        let mut shape_id = if shape_count_a < shape_count_b {
130            world.bodies[body_id_a as usize].head_shape_id
131        } else {
132            world.bodies[body_id_b as usize].head_shape_id
133        };
134        while shape_id != NULL_INDEX {
135            let proxy_key = world.shapes[shape_id as usize].proxy_key;
136            if proxy_key != NULL_INDEX {
137                world.broad_phase.buffer_move(proxy_key);
138            }
139
140            shape_id = world.shapes[shape_id as usize].next_shape_id;
141        }
142    } else {
143        destroy_contacts_between_bodies(world, body_id_a, body_id_b);
144    }
145}
146
147/// (b2Joint_GetCollideConnected)
148pub fn joint_get_collide_connected(world: &World, joint_id: JointId) -> bool {
149    let joint_index = get_joint_full_id(world, joint_id);
150    world.joints[joint_index as usize].collide_connected
151}
152
153/// (b2Joint_SetUserData)
154pub fn joint_set_user_data(world: &mut World, joint_id: JointId, user_data: u64) {
155    let joint_index = get_joint_full_id(world, joint_id);
156    world.joints[joint_index as usize].user_data = user_data;
157}
158
159/// (b2Joint_GetUserData)
160pub fn joint_get_user_data(world: &World, joint_id: JointId) -> u64 {
161    let joint_index = get_joint_full_id(world, joint_id);
162    world.joints[joint_index as usize].user_data
163}
164
165/// (b2Joint_WakeBodies)
166pub fn joint_wake_bodies(world: &mut World, joint_id: JointId) {
167    crate::recording::record_op(world, |rec, _| {
168        crate::recording::write_joint_marker(rec, crate::recording::OP_JOINT_WAKE_BODIES, joint_id)
169    });
170    let joint_index = get_joint_full_id(world, joint_id);
171    let body_id_a = world.joints[joint_index as usize].edges[0].body_id;
172    let body_id_b = world.joints[joint_index as usize].edges[1].body_id;
173
174    wake_body(world, body_id_a);
175    wake_body(world, body_id_b);
176}
177
178/// (b2Joint_GetConstraintForce)
179pub fn joint_get_constraint_force(world: &World, joint_id: JointId) -> Vec2 {
180    let joint_index = get_joint_full_id(world, joint_id);
181    get_joint_constraint_force(world, joint_index)
182}
183
184/// (b2Joint_GetConstraintTorque)
185pub fn joint_get_constraint_torque(world: &World, joint_id: JointId) -> f32 {
186    let joint_index = get_joint_full_id(world, joint_id);
187    get_joint_constraint_torque(world, joint_index)
188}
189
190/// (b2Joint_GetLinearSeparation)
191pub fn joint_get_linear_separation(world: &World, joint_id: JointId) -> f32 {
192    let joint_index = get_joint_full_id(world, joint_id);
193    let base = get_joint_sim_ref(world, joint_index);
194    let joint = &world.joints[joint_index as usize];
195
196    // Relative to body A so the difference stays in float precision far from
197    // the origin
198    let wxf_a = crate::body::get_body_transform(world, joint.edges[0].body_id);
199    let xf_a = to_relative_transform(wxf_a, wxf_a.p);
200    let xf_b = to_relative_transform(
201        crate::body::get_body_transform(world, joint.edges[1].body_id),
202        wxf_a.p,
203    );
204
205    let p_a = transform_point(xf_a, base.local_frame_a.p);
206    let p_b = transform_point(xf_b, base.local_frame_b.p);
207    let dp = sub(p_b, p_a);
208
209    match &base.payload {
210        JointPayload::Distance(distance_joint) => {
211            let length_ = length(dp);
212            if distance_joint.enable_spring {
213                if distance_joint.enable_limit {
214                    if length_ < distance_joint.min_length {
215                        return distance_joint.min_length - length_;
216                    }
217
218                    if length_ > distance_joint.max_length {
219                        return length_ - distance_joint.max_length;
220                    }
221
222                    return 0.0;
223                }
224
225                return 0.0;
226            }
227
228            abs_float(length_ - distance_joint.length)
229        }
230
231        JointPayload::Motor(_) => 0.0,
232
233        JointPayload::Filter => 0.0,
234
235        JointPayload::Prismatic(prismatic_joint) => {
236            let axis_a = rotate_vector(xf_a.q, Vec2 { x: 1.0, y: 0.0 });
237            let perp_a = left_perp(axis_a);
238            let perpendicular_separation = abs_float(dot(perp_a, dp));
239            let mut limit_separation = 0.0;
240
241            if prismatic_joint.enable_limit {
242                let translation = dot(axis_a, dp);
243                if translation < prismatic_joint.lower_translation {
244                    limit_separation = prismatic_joint.lower_translation - translation;
245                }
246
247                if prismatic_joint.upper_translation < translation {
248                    limit_separation = translation - prismatic_joint.upper_translation;
249                }
250            }
251
252            (perpendicular_separation * perpendicular_separation
253                + limit_separation * limit_separation)
254                .sqrt()
255        }
256
257        JointPayload::Revolute(_) => length(dp),
258
259        JointPayload::Weld(weld_joint) => {
260            if weld_joint.linear_hertz == 0.0 {
261                return length(dp);
262            }
263
264            0.0
265        }
266
267        JointPayload::Wheel(wheel_joint) => {
268            let axis_a = rotate_vector(xf_a.q, Vec2 { x: 1.0, y: 0.0 });
269            let perp_a = left_perp(axis_a);
270            let perpendicular_separation = abs_float(dot(perp_a, dp));
271            let mut limit_separation = 0.0;
272
273            if wheel_joint.enable_limit {
274                let translation = dot(axis_a, dp);
275                if translation < wheel_joint.lower_translation {
276                    limit_separation = wheel_joint.lower_translation - translation;
277                }
278
279                if wheel_joint.upper_translation < translation {
280                    limit_separation = translation - wheel_joint.upper_translation;
281                }
282            }
283
284            (perpendicular_separation * perpendicular_separation
285                + limit_separation * limit_separation)
286                .sqrt()
287        }
288    }
289}
290
291/// (b2Joint_GetAngularSeparation)
292pub fn joint_get_angular_separation(world: &World, joint_id: JointId) -> f32 {
293    let joint_index = get_joint_full_id(world, joint_id);
294    let base = get_joint_sim_ref(world, joint_index);
295    let joint = &world.joints[joint_index as usize];
296
297    let q_a = crate::body::get_body_transform(world, joint.edges[0].body_id).q;
298    let q_b = crate::body::get_body_transform(world, joint.edges[1].body_id).q;
299    let relative_angle_ = relative_angle(q_a, q_b);
300
301    match &base.payload {
302        JointPayload::Distance(_) => 0.0,
303
304        JointPayload::Motor(_) => 0.0,
305
306        JointPayload::Filter => 0.0,
307
308        JointPayload::Prismatic(_) => relative_angle_,
309
310        JointPayload::Revolute(revolute_joint) => {
311            if revolute_joint.enable_limit {
312                let angle = relative_angle_;
313                if angle < revolute_joint.lower_angle {
314                    return revolute_joint.lower_angle - angle;
315                }
316
317                if revolute_joint.upper_angle < angle {
318                    return angle - revolute_joint.upper_angle;
319                }
320            }
321
322            0.0
323        }
324
325        JointPayload::Weld(weld_joint) => {
326            if weld_joint.angular_hertz == 0.0 {
327                return relative_angle_;
328            }
329
330            0.0
331        }
332
333        JointPayload::Wheel(_) => 0.0,
334    }
335}
336
337/// (b2Joint_SetConstraintTuning)
338pub fn joint_set_constraint_tuning(
339    world: &mut World,
340    joint_id: JointId,
341    hertz: f32,
342    damping_ratio: f32,
343) {
344    crate::recording::record_op(world, |rec, _| {
345        crate::recording::write_joint_f32_pair(
346            rec,
347            crate::recording::OP_JOINT_SET_CONSTRAINT_TUNING,
348            joint_id,
349            hertz,
350            damping_ratio,
351        )
352    });
353    debug_assert!(is_valid_float(hertz) && hertz >= 0.0);
354    debug_assert!(is_valid_float(damping_ratio) && damping_ratio >= 0.0);
355
356    let joint_index = get_joint_full_id(world, joint_id);
357    let base = get_joint_sim(world, joint_index);
358    base.constraint_hertz = hertz;
359    base.constraint_damping_ratio = damping_ratio;
360}
361
362/// (b2Joint_GetConstraintTuning — C returns through out-pointers)
363pub fn joint_get_constraint_tuning(world: &World, joint_id: JointId) -> (f32, f32) {
364    let joint_index = get_joint_full_id(world, joint_id);
365    let base = get_joint_sim_ref(world, joint_index);
366    (base.constraint_hertz, base.constraint_damping_ratio)
367}
368
369/// (b2Joint_SetForceThreshold)
370pub fn joint_set_force_threshold(world: &mut World, joint_id: JointId, threshold: f32) {
371    crate::recording::record_op(world, |rec, _| {
372        crate::recording::write_joint_f32(
373            rec,
374            crate::recording::OP_JOINT_SET_FORCE_THRESHOLD,
375            joint_id,
376            threshold,
377        )
378    });
379    debug_assert!(is_valid_float(threshold) && threshold >= 0.0);
380
381    let joint_index = get_joint_full_id(world, joint_id);
382    let base = get_joint_sim(world, joint_index);
383    base.force_threshold = threshold;
384}
385
386/// (b2Joint_GetForceThreshold)
387pub fn joint_get_force_threshold(world: &World, joint_id: JointId) -> f32 {
388    let joint_index = get_joint_full_id(world, joint_id);
389    get_joint_sim_ref(world, joint_index).force_threshold
390}
391
392/// (b2Joint_SetTorqueThreshold)
393pub fn joint_set_torque_threshold(world: &mut World, joint_id: JointId, threshold: f32) {
394    crate::recording::record_op(world, |rec, _| {
395        crate::recording::write_joint_f32(
396            rec,
397            crate::recording::OP_JOINT_SET_TORQUE_THRESHOLD,
398            joint_id,
399            threshold,
400        )
401    });
402    debug_assert!(is_valid_float(threshold) && threshold >= 0.0);
403
404    let joint_index = get_joint_full_id(world, joint_id);
405    let base = get_joint_sim(world, joint_index);
406    base.torque_threshold = threshold;
407}
408
409/// (b2Joint_GetTorqueThreshold)
410pub fn joint_get_torque_threshold(world: &World, joint_id: JointId) -> f32 {
411    let joint_index = get_joint_full_id(world, joint_id);
412    get_joint_sim_ref(world, joint_index).torque_threshold
413}