Skip to main content

boxdd/joints/
wheel.rs

1#![allow(rustdoc::broken_intra_doc_links)]
2use crate::types::BodyId;
3use crate::world::World;
4use boxdd_sys::ffi;
5
6use super::{Joint, JointBase, OwnedJoint, raw_body_id};
7use crate::error::ApiResult;
8
9// Wheel joint
10#[derive(Clone, Debug)]
11/// Wheel (suspension) joint definition (maps to `b2WheelJointDef`).
12///
13/// Constrains motion along an axis with suspension (spring + damping) and
14/// optional motor around the wheel axis. Use with `World::create_wheel_joint(_id)`
15/// or `World::wheel(...).build()`.
16pub struct WheelJointDef(pub(crate) ffi::b2WheelJointDef);
17
18impl WheelJointDef {
19    pub fn new(base: JointBase) -> Self {
20        let mut def: ffi::b2WheelJointDef = unsafe { ffi::b2DefaultWheelJointDef() };
21        def.base = base.0;
22        Self(def)
23    }
24
25    #[inline]
26    pub fn from_raw(raw: ffi::b2WheelJointDef) -> Self {
27        Self(raw)
28    }
29
30    #[inline]
31    pub fn base(&self) -> JointBase {
32        JointBase(self.0.base)
33    }
34
35    #[inline]
36    pub fn spring_enabled(&self) -> bool {
37        self.0.enableSpring
38    }
39
40    #[inline]
41    pub fn spring_hertz(&self) -> f32 {
42        self.0.hertz
43    }
44
45    #[inline]
46    pub fn spring_damping_ratio(&self) -> f32 {
47        self.0.dampingRatio
48    }
49
50    #[inline]
51    pub fn limit_enabled(&self) -> bool {
52        self.0.enableLimit
53    }
54
55    #[inline]
56    pub fn minimum_translation(&self) -> f32 {
57        self.0.lowerTranslation
58    }
59
60    #[inline]
61    pub fn maximum_translation(&self) -> f32 {
62        self.0.upperTranslation
63    }
64
65    #[inline]
66    pub fn motor_enabled(&self) -> bool {
67        self.0.enableMotor
68    }
69
70    #[inline]
71    pub fn maximum_motor_torque(&self) -> f32 {
72        self.0.maxMotorTorque
73    }
74
75    #[inline]
76    pub fn target_motor_speed(&self) -> f32 {
77        self.0.motorSpeed
78    }
79
80    #[inline]
81    pub fn into_raw(self) -> ffi::b2WheelJointDef {
82        self.0
83    }
84
85    #[inline]
86    pub fn validate(&self) -> ApiResult<()> {
87        super::check_wheel_joint_def_valid(self)
88    }
89
90    /// Enable/disable suspension spring.
91    pub fn enable_spring(mut self, flag: bool) -> Self {
92        self.0.enableSpring = flag;
93        self
94    }
95    /// Spring stiffness in Hertz.
96    pub fn hertz(mut self, v: f32) -> Self {
97        self.0.hertz = v;
98        self
99    }
100    /// Spring damping ratio \[0,1].
101    pub fn damping_ratio(mut self, v: f32) -> Self {
102        self.0.dampingRatio = v;
103        self
104    }
105    /// Enable/disable translation limits.
106    pub fn enable_limit(mut self, flag: bool) -> Self {
107        self.0.enableLimit = flag;
108        self
109    }
110    /// Lower translation limit (meters).
111    pub fn lower_translation(mut self, v: f32) -> Self {
112        self.0.lowerTranslation = v;
113        self
114    }
115    /// Upper translation limit (meters).
116    pub fn upper_translation(mut self, v: f32) -> Self {
117        self.0.upperTranslation = v;
118        self
119    }
120    /// Enable/disable wheel motor.
121    pub fn enable_motor(mut self, flag: bool) -> Self {
122        self.0.enableMotor = flag;
123        self
124    }
125    /// Maximum motor torque (N·m).
126    pub fn max_motor_torque(mut self, v: f32) -> Self {
127        self.0.maxMotorTorque = v;
128        self
129    }
130    /// Motor speed (rad/s).
131    pub fn motor_speed(mut self, v: f32) -> Self {
132        self.0.motorSpeed = v;
133        self
134    }
135    /// Convenience: motor speed in degrees/sec.
136    pub fn motor_speed_deg(mut self, speed_deg_per_s: f32) -> Self {
137        self.0.motorSpeed = speed_deg_per_s * (core::f32::consts::PI / 180.0);
138        self
139    }
140}
141
142/// Fluent builder for wheel joints using world anchors and axis.
143pub struct WheelJointBuilder<'w> {
144    pub(crate) world: &'w mut World,
145    pub(crate) body_a: BodyId,
146    pub(crate) body_b: BodyId,
147    pub(crate) anchor_a_world: Option<ffi::b2Vec2>,
148    pub(crate) anchor_b_world: Option<ffi::b2Vec2>,
149    pub(crate) axis_world: Option<ffi::b2Vec2>,
150    pub(crate) def: WheelJointDef,
151}
152
153impl<'w> WheelJointBuilder<'w> {
154    /// Set world-space anchors for A and B.
155    pub fn anchors_world<VA: Into<crate::types::Vec2>, VB: Into<crate::types::Vec2>>(
156        mut self,
157        a: VA,
158        b: VB,
159    ) -> Self {
160        self.anchor_a_world = Some(a.into().into_raw());
161        self.anchor_b_world = Some(b.into().into_raw());
162        self
163    }
164    /// Set wheel axis in world space.
165    pub fn axis_world<V: Into<crate::types::Vec2>>(mut self, axis: V) -> Self {
166        self.axis_world = Some(axis.into().into_raw());
167        self
168    }
169    pub fn limit(mut self, lower: f32, upper: f32) -> Self {
170        self.def = self
171            .def
172            .enable_limit(true)
173            .lower_translation(lower)
174            .upper_translation(upper);
175        self
176    }
177    pub fn motor(mut self, max_torque: f32, speed: f32) -> Self {
178        self.def = self
179            .def
180            .enable_motor(true)
181            .max_motor_torque(max_torque)
182            .motor_speed(speed);
183        self
184    }
185    pub fn motor_deg(mut self, max_torque: f32, speed_deg: f32) -> Self {
186        self.def = self
187            .def
188            .enable_motor(true)
189            .max_motor_torque(max_torque)
190            .motor_speed_deg(speed_deg);
191        self
192    }
193    pub fn spring(mut self, hertz: f32, damping_ratio: f32) -> Self {
194        self.def = self
195            .def
196            .enable_spring(true)
197            .hertz(hertz)
198            .damping_ratio(damping_ratio);
199        self
200    }
201    pub fn collide_connected(mut self, flag: bool) -> Self {
202        self.def.0.base.collideConnected = flag;
203        self
204    }
205
206    /// Convenience: enable limit and motor together.
207    /// - lower/upper: meters
208    /// - max_torque: N·m; speed: rad/s
209    pub fn with_limit_and_motor(
210        mut self,
211        lower: f32,
212        upper: f32,
213        max_torque: f32,
214        speed: f32,
215    ) -> Self {
216        self = self.limit(lower, upper);
217        self = self.motor(max_torque, speed);
218        self
219    }
220    /// Convenience: enable limit and motor together (motor speed in degrees/sec).
221    /// - lower/upper: meters
222    /// - max_torque: N·m; speed_deg: deg/s
223    pub fn with_limit_and_motor_deg(
224        mut self,
225        lower: f32,
226        upper: f32,
227        max_torque: f32,
228        speed_deg: f32,
229    ) -> Self {
230        self = self.limit(lower, upper);
231        self = self.motor_deg(max_torque, speed_deg);
232        self
233    }
234    /// Convenience: enable limit and spring together.
235    /// - lower/upper: meters
236    /// - hertz: stiffness (Hz), typical 4–20; damping_ratio: \[0,1], typical 0.1–0.7
237    pub fn with_limit_and_spring(
238        mut self,
239        lower: f32,
240        upper: f32,
241        hertz: f32,
242        damping_ratio: f32,
243    ) -> Self {
244        self = self.limit(lower, upper);
245        self = self.spring(hertz, damping_ratio);
246        self
247    }
248    /// Convenience: enable motor and spring together.
249    /// - max_torque: N·m; speed: rad/s; hertz: Hz; damping_ratio: \[0,1]
250    pub fn with_motor_and_spring(
251        mut self,
252        max_torque: f32,
253        speed: f32,
254        hertz: f32,
255        damping_ratio: f32,
256    ) -> Self {
257        self = self.motor(max_torque, speed);
258        self = self.spring(hertz, damping_ratio);
259        self
260    }
261    /// Convenience: enable motor and spring together (motor speed in degrees/sec).
262    /// - max_torque: N·m; speed_deg: deg/s; hertz: Hz; damping_ratio: \[0,1]
263    pub fn with_motor_and_spring_deg(
264        mut self,
265        max_torque: f32,
266        speed_deg: f32,
267        hertz: f32,
268        damping_ratio: f32,
269    ) -> Self {
270        self = self.motor_deg(max_torque, speed_deg);
271        self = self.spring(hertz, damping_ratio);
272        self
273    }
274    /// Convenience: enable limit, motor, and spring together.
275    /// - lower/upper: meters
276    /// - max_torque: N·m; speed: rad/s; hertz: Hz; damping_ratio: \[0,1]
277    pub fn with_limit_motor_spring(
278        mut self,
279        lower: f32,
280        upper: f32,
281        max_torque: f32,
282        speed: f32,
283        hertz: f32,
284        damping_ratio: f32,
285    ) -> Self {
286        self = self.limit(lower, upper);
287        self = self.motor(max_torque, speed);
288        self = self.spring(hertz, damping_ratio);
289        self
290    }
291    /// Convenience: enable limit, motor (deg/s), and spring together.
292    /// - lower/upper: meters
293    /// - max_torque: N·m; speed_deg: deg/s; hertz: Hz; damping_ratio: \[0,1]
294    pub fn with_limit_motor_spring_deg(
295        mut self,
296        lower: f32,
297        upper: f32,
298        max_torque: f32,
299        speed_deg: f32,
300        hertz: f32,
301        damping_ratio: f32,
302    ) -> Self {
303        self = self.limit(lower, upper);
304        self = self.motor_deg(max_torque, speed_deg);
305        self = self.spring(hertz, damping_ratio);
306        self
307    }
308
309    #[must_use]
310    pub fn build(mut self) -> Joint<'w> {
311        crate::core::debug_checks::assert_body_valid(self.body_a);
312        crate::core::debug_checks::assert_body_valid(self.body_b);
313        // Defaults: anchors = body positions, axis = x
314        let ta = unsafe { ffi::b2Body_GetTransform(raw_body_id(self.body_a)) };
315        let tb = unsafe { ffi::b2Body_GetTransform(raw_body_id(self.body_b)) };
316        let aw = self.anchor_a_world.unwrap_or(ta.p);
317        let bw = self.anchor_b_world.unwrap_or(tb.p);
318        let axis = self.axis_world.unwrap_or(ffi::b2Vec2 { x: 1.0, y: 0.0 });
319        let la = crate::core::math::world_to_local_point(ta, aw);
320        let lb = crate::core::math::world_to_local_point(tb, bw);
321        let ra = crate::core::math::world_axis_to_local_rot(ta, axis);
322        let rb = crate::core::math::world_axis_to_local_rot(tb, axis);
323        self.def.0.base.bodyIdA = raw_body_id(self.body_a);
324        self.def.0.base.bodyIdB = raw_body_id(self.body_b);
325        self.def.0.base.localFrameA = ffi::b2Transform { p: la, q: ra };
326        self.def.0.base.localFrameB = ffi::b2Transform { p: lb, q: rb };
327        self.world.create_wheel_joint(&self.def)
328    }
329
330    pub fn try_build(mut self) -> ApiResult<Joint<'w>> {
331        crate::core::debug_checks::check_body_valid(self.body_a)?;
332        crate::core::debug_checks::check_body_valid(self.body_b)?;
333        let ta = unsafe { ffi::b2Body_GetTransform(raw_body_id(self.body_a)) };
334        let tb = unsafe { ffi::b2Body_GetTransform(raw_body_id(self.body_b)) };
335        let aw = self.anchor_a_world.unwrap_or(ta.p);
336        let bw = self.anchor_b_world.unwrap_or(tb.p);
337        let axis = self.axis_world.unwrap_or(ffi::b2Vec2 { x: 1.0, y: 0.0 });
338        let la = crate::core::math::world_to_local_point(ta, aw);
339        let lb = crate::core::math::world_to_local_point(tb, bw);
340        let ra = crate::core::math::world_axis_to_local_rot(ta, axis);
341        let rb = crate::core::math::world_axis_to_local_rot(tb, axis);
342        self.def.0.base.bodyIdA = raw_body_id(self.body_a);
343        self.def.0.base.bodyIdB = raw_body_id(self.body_b);
344        self.def.0.base.localFrameA = ffi::b2Transform { p: la, q: ra };
345        self.def.0.base.localFrameB = ffi::b2Transform { p: lb, q: rb };
346        self.world.try_create_wheel_joint(&self.def)
347    }
348
349    #[must_use]
350    pub fn build_owned(mut self) -> OwnedJoint {
351        crate::core::debug_checks::assert_body_valid(self.body_a);
352        crate::core::debug_checks::assert_body_valid(self.body_b);
353        // Defaults: anchors = body positions, axis = x
354        let ta = unsafe { ffi::b2Body_GetTransform(raw_body_id(self.body_a)) };
355        let tb = unsafe { ffi::b2Body_GetTransform(raw_body_id(self.body_b)) };
356        let aw = self.anchor_a_world.unwrap_or(ta.p);
357        let bw = self.anchor_b_world.unwrap_or(tb.p);
358        let axis = self.axis_world.unwrap_or(ffi::b2Vec2 { x: 1.0, y: 0.0 });
359        let la = crate::core::math::world_to_local_point(ta, aw);
360        let lb = crate::core::math::world_to_local_point(tb, bw);
361        let ra = crate::core::math::world_axis_to_local_rot(ta, axis);
362        let rb = crate::core::math::world_axis_to_local_rot(tb, axis);
363        self.def.0.base.bodyIdA = raw_body_id(self.body_a);
364        self.def.0.base.bodyIdB = raw_body_id(self.body_b);
365        self.def.0.base.localFrameA = ffi::b2Transform { p: la, q: ra };
366        self.def.0.base.localFrameB = ffi::b2Transform { p: lb, q: rb };
367        self.world.create_wheel_joint_owned(&self.def)
368    }
369
370    pub fn try_build_owned(mut self) -> ApiResult<OwnedJoint> {
371        crate::core::debug_checks::check_body_valid(self.body_a)?;
372        crate::core::debug_checks::check_body_valid(self.body_b)?;
373        let ta = unsafe { ffi::b2Body_GetTransform(raw_body_id(self.body_a)) };
374        let tb = unsafe { ffi::b2Body_GetTransform(raw_body_id(self.body_b)) };
375        let aw = self.anchor_a_world.unwrap_or(ta.p);
376        let bw = self.anchor_b_world.unwrap_or(tb.p);
377        let axis = self.axis_world.unwrap_or(ffi::b2Vec2 { x: 1.0, y: 0.0 });
378        let la = crate::core::math::world_to_local_point(ta, aw);
379        let lb = crate::core::math::world_to_local_point(tb, bw);
380        let ra = crate::core::math::world_axis_to_local_rot(ta, axis);
381        let rb = crate::core::math::world_axis_to_local_rot(tb, axis);
382        self.def.0.base.bodyIdA = raw_body_id(self.body_a);
383        self.def.0.base.bodyIdB = raw_body_id(self.body_b);
384        self.def.0.base.localFrameA = ffi::b2Transform { p: la, q: ra };
385        self.def.0.base.localFrameB = ffi::b2Transform { p: lb, q: rb };
386        self.world.try_create_wheel_joint_owned(&self.def)
387    }
388}