1#![allow(dead_code)]
12
13#[derive(Debug, Clone, Copy, PartialEq)]
19pub struct Quaternion {
20 pub w: f64,
22 pub x: f64,
24 pub y: f64,
26 pub z: f64,
28}
29
30impl Quaternion {
31 pub fn new(w: f64, x: f64, y: f64, z: f64) -> Self {
33 Self { w, x, y, z }
34 }
35
36 pub fn identity() -> Self {
38 Self::new(1.0, 0.0, 0.0, 0.0)
39 }
40
41 pub fn from_axis_angle(axis: [f64; 3], angle: f64) -> Self {
48 let len = (axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]).sqrt();
49 let inv = 1.0 / len;
50 let nx = axis[0] * inv;
51 let ny = axis[1] * inv;
52 let nz = axis[2] * inv;
53 let half = angle * 0.5;
54 let s = half.sin();
55 Self::new(half.cos(), nx * s, ny * s, nz * s)
56 }
57
58 pub fn mul(&self, other: &Quaternion) -> Quaternion {
60 Quaternion::new(
61 self.w * other.w - self.x * other.x - self.y * other.y - self.z * other.z,
62 self.w * other.x + self.x * other.w + self.y * other.z - self.z * other.y,
63 self.w * other.y - self.x * other.z + self.y * other.w + self.z * other.x,
64 self.w * other.z + self.x * other.y - self.y * other.x + self.z * other.w,
65 )
66 }
67
68 pub fn conjugate(&self) -> Quaternion {
70 Quaternion::new(self.w, -self.x, -self.y, -self.z)
71 }
72
73 pub fn norm(&self) -> f64 {
75 (self.w * self.w + self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
76 }
77
78 pub fn normalize(&self) -> Quaternion {
81 let n = self.norm();
82 if n < f64::EPSILON {
83 return Quaternion::identity();
84 }
85 let inv = 1.0 / n;
86 Quaternion::new(self.w * inv, self.x * inv, self.y * inv, self.z * inv)
87 }
88
89 pub fn rotate_vector(&self, v: [f64; 3]) -> [f64; 3] {
91 let qv = Quaternion::new(0.0, v[0], v[1], v[2]);
92 let res = self.mul(&qv).mul(&self.conjugate());
93 [res.x, res.y, res.z]
94 }
95
96 pub fn dot(&self, other: &Quaternion) -> f64 {
98 self.w * other.w + self.x * other.x + self.y * other.y + self.z * other.z
99 }
100
101 pub fn slerp(&self, other: &Quaternion, t: f64) -> Quaternion {
104 let mut d = self.dot(other);
105 let other_adj = if d < 0.0 {
107 d = -d;
108 Quaternion::new(-other.w, -other.x, -other.y, -other.z)
109 } else {
110 *other
111 };
112
113 if d > 1.0 - 1e-10 {
114 let w = self.w + t * (other_adj.w - self.w);
116 let x = self.x + t * (other_adj.x - self.x);
117 let y = self.y + t * (other_adj.y - self.y);
118 let z = self.z + t * (other_adj.z - self.z);
119 return Quaternion::new(w, x, y, z).normalize();
120 }
121
122 let theta = d.acos();
123 let sin_theta = theta.sin();
124 let s0 = ((1.0 - t) * theta).sin() / sin_theta;
125 let s1 = (t * theta).sin() / sin_theta;
126 Quaternion::new(
127 s0 * self.w + s1 * other_adj.w,
128 s0 * self.x + s1 * other_adj.x,
129 s0 * self.y + s1 * other_adj.y,
130 s0 * self.z + s1 * other_adj.z,
131 )
132 }
133
134 pub fn to_rotation_matrix(&self) -> [[f64; 3]; 3] {
136 let q = self.normalize();
137 let (w, x, y, z) = (q.w, q.x, q.y, q.z);
138 [
139 [
140 1.0 - 2.0 * (y * y + z * z),
141 2.0 * (x * y - w * z),
142 2.0 * (x * z + w * y),
143 ],
144 [
145 2.0 * (x * y + w * z),
146 1.0 - 2.0 * (x * x + z * z),
147 2.0 * (y * z - w * x),
148 ],
149 [
150 2.0 * (x * z - w * y),
151 2.0 * (y * z + w * x),
152 1.0 - 2.0 * (x * x + y * y),
153 ],
154 ]
155 }
156
157 pub fn from_rotation_matrix(m: &[[f64; 3]; 3]) -> Quaternion {
160 let trace = m[0][0] + m[1][1] + m[2][2];
161 if trace > 0.0 {
162 let s = 0.5 / (trace + 1.0).sqrt();
163 Quaternion::new(
164 0.25 / s,
165 (m[2][1] - m[1][2]) * s,
166 (m[0][2] - m[2][0]) * s,
167 (m[1][0] - m[0][1]) * s,
168 )
169 } else if m[0][0] > m[1][1] && m[0][0] > m[2][2] {
170 let s = 2.0 * (1.0 + m[0][0] - m[1][1] - m[2][2]).sqrt();
171 Quaternion::new(
172 (m[2][1] - m[1][2]) / s,
173 0.25 * s,
174 (m[0][1] + m[1][0]) / s,
175 (m[0][2] + m[2][0]) / s,
176 )
177 } else if m[1][1] > m[2][2] {
178 let s = 2.0 * (1.0 + m[1][1] - m[0][0] - m[2][2]).sqrt();
179 Quaternion::new(
180 (m[0][2] - m[2][0]) / s,
181 (m[0][1] + m[1][0]) / s,
182 0.25 * s,
183 (m[1][2] + m[2][1]) / s,
184 )
185 } else {
186 let s = 2.0 * (1.0 + m[2][2] - m[0][0] - m[1][1]).sqrt();
187 Quaternion::new(
188 (m[1][0] - m[0][1]) / s,
189 (m[0][2] + m[2][0]) / s,
190 (m[1][2] + m[2][1]) / s,
191 0.25 * s,
192 )
193 }
194 }
195
196 pub fn exp(&self) -> Quaternion {
199 let v_norm = (self.x * self.x + self.y * self.y + self.z * self.z).sqrt();
200 let ew = self.w.exp();
201 if v_norm < f64::EPSILON {
202 return Quaternion::new(ew, 0.0, 0.0, 0.0);
203 }
204 let s = ew * v_norm.sin() / v_norm;
205 Quaternion::new(ew * v_norm.cos(), self.x * s, self.y * s, self.z * s)
206 }
207
208 pub fn ln(&self) -> Quaternion {
211 let n = self.norm();
212 let v_norm = (self.x * self.x + self.y * self.y + self.z * self.z).sqrt();
213 if v_norm < f64::EPSILON {
214 return Quaternion::new(n.ln(), 0.0, 0.0, 0.0);
215 }
216 let theta = (self.w / n).acos();
217 let s = theta / v_norm;
218 Quaternion::new(n.ln(), self.x * s, self.y * s, self.z * s)
219 }
220}
221
222#[derive(Debug, Clone, Copy, PartialEq)]
228pub struct DualNumber {
229 pub real: f64,
231 pub dual: f64,
233}
234
235impl DualNumber {
236 pub fn new(real: f64, dual: f64) -> Self {
238 Self { real, dual }
239 }
240
241 pub fn add(&self, other: &DualNumber) -> DualNumber {
243 DualNumber::new(self.real + other.real, self.dual + other.dual)
244 }
245
246 pub fn mul(&self, other: &DualNumber) -> DualNumber {
248 DualNumber::new(
249 self.real * other.real,
250 self.real * other.dual + self.dual * other.real,
251 )
252 }
253
254 pub fn conjugate(&self) -> DualNumber {
256 DualNumber::new(self.real, -self.dual)
257 }
258}
259
260#[derive(Debug, Clone, Copy, PartialEq)]
270pub struct DualQuaternion {
271 pub real: Quaternion,
273 pub dual: Quaternion,
275}
276
277impl DualQuaternion {
278 pub fn identity() -> Self {
280 Self {
281 real: Quaternion::identity(),
282 dual: Quaternion::new(0.0, 0.0, 0.0, 0.0),
283 }
284 }
285
286 pub fn from_rotation_translation(q: Quaternion, t: [f64; 3]) -> Self {
291 let t_quat = Quaternion::new(0.0, t[0], t[1], t[2]);
292 let dual = t_quat.mul(&q).scale(0.5);
293 Self { real: q, dual }
294 }
295
296 pub fn mul(&self, other: &DualQuaternion) -> DualQuaternion {
298 DualQuaternion {
299 real: self.real.mul(&other.real),
300 dual: self.real.mul(&other.dual).add(&self.dual.mul(&other.real)),
301 }
302 }
303
304 pub fn conjugate(&self) -> DualQuaternion {
306 DualQuaternion {
307 real: self.real.conjugate(),
308 dual: self.dual.conjugate(),
309 }
310 }
311
312 pub fn normalize(&self) -> DualQuaternion {
314 let n = self.real.norm();
315 if n < f64::EPSILON {
316 return DualQuaternion::identity();
317 }
318 let inv = 1.0 / n;
319 DualQuaternion {
320 real: self.real.scale(inv),
321 dual: self.dual.scale(inv),
322 }
323 }
324
325 pub fn norm(&self) -> f64 {
327 self.real.norm()
328 }
329
330 pub fn transform_point(&self, p: [f64; 3]) -> [f64; 3] {
334 let dq = self.normalize();
335 let rotated = dq.real.rotate_vector(p);
337 let t = dq.to_translation();
339 [rotated[0] + t[0], rotated[1] + t[1], rotated[2] + t[2]]
340 }
341
342 pub fn to_rotation(&self) -> Quaternion {
344 self.real.normalize()
345 }
346
347 pub fn to_translation(&self) -> [f64; 3] {
351 let dq = self.normalize();
352 let t = dq.dual.mul(&dq.real.conjugate()).scale(2.0);
353 [t.x, t.y, t.z]
354 }
355
356 pub fn sclerp(&self, other: &DualQuaternion, t: f64) -> DualQuaternion {
360 let self_inv = self.conjugate();
361 let diff = self_inv.mul(other).normalize();
362 self.mul(&diff.pow(t))
363 }
364
365 pub fn from_twist(axis: [f64; 3], angle: f64, pitch: f64, point: [f64; 3]) -> DualQuaternion {
380 let q_rot = Quaternion::from_axis_angle(axis, angle);
382
383 let d = pitch * angle;
385
386 let t_vec = [axis[0] * d, axis[1] * d, axis[2] * d];
388
389 let rotated_point = q_rot.rotate_vector(point);
393 let correction = [
394 point[0] - rotated_point[0],
395 point[1] - rotated_point[1],
396 point[2] - rotated_point[2],
397 ];
398
399 let full_t = [
401 t_vec[0] + correction[0],
402 t_vec[1] + correction[1],
403 t_vec[2] + correction[2],
404 ];
405
406 DualQuaternion::from_rotation_translation(q_rot, full_t)
407 }
408
409 fn pow(&self, t: f64) -> DualQuaternion {
413 let sm = ScrewMotion::from_dual_quaternion(self);
415 DualQuaternion::from_twist(sm.axis_dir, sm.angle * t, sm.pitch, sm.axis_point)
416 }
417}
418
419impl Quaternion {
422 fn scale(&self, s: f64) -> Quaternion {
423 Quaternion::new(self.w * s, self.x * s, self.y * s, self.z * s)
424 }
425
426 fn add(&self, other: &Quaternion) -> Quaternion {
427 Quaternion::new(
428 self.w + other.w,
429 self.x + other.x,
430 self.y + other.y,
431 self.z + other.z,
432 )
433 }
434}
435
436#[derive(Debug, Clone, Copy)]
446pub struct ScrewMotion {
447 pub axis_dir: [f64; 3],
449 pub axis_point: [f64; 3],
451 pub angle: f64,
453 pub pitch: f64,
455}
456
457impl ScrewMotion {
458 pub fn to_dual_quaternion(&self) -> DualQuaternion {
460 DualQuaternion::from_twist(self.axis_dir, self.angle, self.pitch, self.axis_point)
461 }
462
463 pub fn from_dual_quaternion(dq: &DualQuaternion) -> ScrewMotion {
466 let dq = dq.normalize();
467 let q = dq.real;
469 let w_clamped = q.w.clamp(-1.0, 1.0);
471 let angle = 2.0 * w_clamped.acos();
472 let sin_half = (1.0 - w_clamped * w_clamped).sqrt();
473
474 let (axis_dir, axis_point, pitch) = if sin_half < 1e-10 {
475 let t = dq.to_translation();
477 let t_norm = (t[0] * t[0] + t[1] * t[1] + t[2] * t[2]).sqrt();
478 let dir = if t_norm < f64::EPSILON {
479 [0.0, 0.0, 1.0]
480 } else {
481 [t[0] / t_norm, t[1] / t_norm, t[2] / t_norm]
482 };
483 ([dir[0], dir[1], dir[2]], [0.0_f64, 0.0, 0.0], t_norm)
484 } else {
485 let inv = 1.0 / sin_half;
486 let axis = [q.x * inv, q.y * inv, q.z * inv];
487
488 let t = dq.to_translation();
491 let pitch_val = t[0] * axis[0] + t[1] * axis[1] + t[2] * axis[2];
493 let t_perp = [
496 t[0] - pitch_val * axis[0],
497 t[1] - pitch_val * axis[1],
498 t[2] - pitch_val * axis[2],
499 ];
500 let cross = [
504 axis[1] * t_perp[2] - axis[2] * t_perp[1],
505 axis[2] * t_perp[0] - axis[0] * t_perp[2],
506 axis[0] * t_perp[1] - axis[1] * t_perp[0],
507 ];
508 let one_minus_cos = 2.0 * sin_half * sin_half; let sin_theta = 2.0 * sin_half * w_clamped; let denom = 2.0 * one_minus_cos;
511 let p = if denom.abs() < 1e-10 {
512 [0.0, 0.0, 0.0]
513 } else {
514 [
515 (one_minus_cos * t_perp[0] + sin_theta * cross[0]) / denom,
516 (one_minus_cos * t_perp[1] + sin_theta * cross[1]) / denom,
517 (one_minus_cos * t_perp[2] + sin_theta * cross[2]) / denom,
518 ]
519 };
520 let pitch_per_rad = if angle.abs() < 1e-10 {
521 0.0
522 } else {
523 pitch_val / angle
524 };
525 (axis, p, pitch_per_rad)
526 };
527
528 ScrewMotion {
529 axis_dir,
530 axis_point,
531 angle,
532 pitch,
533 }
534 }
535}
536
537#[allow(dead_code)]
548pub fn dual_quaternion_blend(dqs: &[DualQuaternion], weights: &[f64]) -> DualQuaternion {
549 let n = dqs.len().min(weights.len());
550 if n == 0 {
551 return DualQuaternion::identity();
552 }
553 let mut sum_real = Quaternion::new(0.0, 0.0, 0.0, 0.0);
554 let mut sum_dual = Quaternion::new(0.0, 0.0, 0.0, 0.0);
555 let first_real = dqs[0].real;
556 for i in 0..n {
557 let dq = if dqs[i].real.dot(&first_real) < 0.0 {
559 DualQuaternion {
560 real: dqs[i].real.scale(-1.0),
561 dual: dqs[i].dual.scale(-1.0),
562 }
563 } else {
564 dqs[i]
565 };
566 let w = weights[i];
567 sum_real = sum_real.add(&dq.real.scale(w));
568 sum_dual = sum_dual.add(&dq.dual.scale(w));
569 }
570 DualQuaternion {
571 real: sum_real,
572 dual: sum_dual,
573 }
574 .normalize()
575}
576
577impl DualQuaternion {
582 #[allow(dead_code)]
587 pub fn nlerp(&self, other: &DualQuaternion, t: f64) -> DualQuaternion {
588 let a = self.normalize();
589 let b = other.normalize();
590 let b = if a.real.dot(&b.real) < 0.0 {
592 DualQuaternion {
593 real: b.real.scale(-1.0),
594 dual: b.dual.scale(-1.0),
595 }
596 } else {
597 b
598 };
599 let real = a.real.scale(1.0 - t).add(&b.real.scale(t));
600 let dual = a.dual.scale(1.0 - t).add(&b.dual.scale(t));
601 DualQuaternion { real, dual }.normalize()
602 }
603
604 #[allow(dead_code)]
609 pub fn compose_transforms(&self, other: &DualQuaternion) -> DualQuaternion {
610 other.mul(self)
611 }
612
613 #[allow(dead_code)]
615 pub fn invert(&self) -> DualQuaternion {
616 self.conjugate()
617 }
618
619 #[allow(dead_code)]
621 pub fn transform_points(&self, points: &[[f64; 3]]) -> Vec<[f64; 3]> {
622 points.iter().map(|&p| self.transform_point(p)).collect()
623 }
624
625 #[allow(dead_code)]
627 pub fn rotation_angle(&self) -> f64 {
628 let q = self.real.normalize();
629 2.0 * q.w.clamp(-1.0, 1.0).acos()
630 }
631
632 #[allow(dead_code)]
636 pub fn rotation_axis(&self) -> [f64; 3] {
637 let q = self.real.normalize();
638 let s = (1.0 - q.w * q.w).sqrt();
639 if s < 1e-12 {
640 [0.0, 0.0, 1.0]
641 } else {
642 [q.x / s, q.y / s, q.z / s]
643 }
644 }
645}
646
647#[allow(dead_code)]
656pub fn sclerp_sequence(
657 start: &DualQuaternion,
658 end: &DualQuaternion,
659 n: usize,
660) -> Vec<DualQuaternion> {
661 if n == 0 {
662 return Vec::new();
663 }
664 if n == 1 {
665 return vec![*start];
666 }
667 (0..n)
668 .map(|i| {
669 let t = i as f64 / (n - 1) as f64;
670 start.sclerp(end, t)
671 })
672 .collect()
673}
674
675#[derive(Debug, Clone, Copy)]
685pub struct PluckerLine {
686 pub direction: [f64; 3],
688 pub moment: [f64; 3],
690}
691
692impl PluckerLine {
693 pub fn from_two_points(p: [f64; 3], q: [f64; 3]) -> Self {
695 let d = [q[0] - p[0], q[1] - p[1], q[2] - p[2]];
696 let len = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
697 let direction = if len < f64::EPSILON {
698 [0.0, 0.0, 1.0]
699 } else {
700 [d[0] / len, d[1] / len, d[2] / len]
701 };
702 let moment = [
704 p[1] * direction[2] - p[2] * direction[1],
705 p[2] * direction[0] - p[0] * direction[2],
706 p[0] * direction[1] - p[1] * direction[0],
707 ];
708 Self { direction, moment }
709 }
710
711 pub fn from_point_direction(p: [f64; 3], d: [f64; 3]) -> Self {
713 let len = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
714 let direction = if len < f64::EPSILON {
715 [0.0, 0.0, 1.0]
716 } else {
717 [d[0] / len, d[1] / len, d[2] / len]
718 };
719 let moment = [
720 p[1] * direction[2] - p[2] * direction[1],
721 p[2] * direction[0] - p[0] * direction[2],
722 p[0] * direction[1] - p[1] * direction[0],
723 ];
724 Self { direction, moment }
725 }
726
727 pub fn distance_to_line(&self, other: &PluckerLine) -> f64 {
729 let cross_d = [
732 self.direction[1] * other.direction[2] - self.direction[2] * other.direction[1],
733 self.direction[2] * other.direction[0] - self.direction[0] * other.direction[2],
734 self.direction[0] * other.direction[1] - self.direction[1] * other.direction[0],
735 ];
736 let cross_len =
737 (cross_d[0] * cross_d[0] + cross_d[1] * cross_d[1] + cross_d[2] * cross_d[2]).sqrt();
738
739 let dot1 = self.direction[0] * other.moment[0]
740 + self.direction[1] * other.moment[1]
741 + self.direction[2] * other.moment[2];
742 let dot2 = other.direction[0] * self.moment[0]
743 + other.direction[1] * self.moment[1]
744 + other.direction[2] * self.moment[2];
745
746 if cross_len < 1e-12 {
747 let p1 = [
750 self.direction[1] * self.moment[2] - self.direction[2] * self.moment[1],
751 self.direction[2] * self.moment[0] - self.direction[0] * self.moment[2],
752 self.direction[0] * self.moment[1] - self.direction[1] * self.moment[0],
753 ];
754 let p2 = [
755 other.direction[1] * other.moment[2] - other.direction[2] * other.moment[1],
756 other.direction[2] * other.moment[0] - other.direction[0] * other.moment[2],
757 other.direction[0] * other.moment[1] - other.direction[1] * other.moment[0],
758 ];
759 let dp = [p1[0] - p2[0], p1[1] - p2[1], p1[2] - p2[2]];
760 let cx = dp[1] * self.direction[2] - dp[2] * self.direction[1];
762 let cy = dp[2] * self.direction[0] - dp[0] * self.direction[2];
763 let cz = dp[0] * self.direction[1] - dp[1] * self.direction[0];
764 (cx * cx + cy * cy + cz * cz).sqrt()
765 } else {
766 (dot1 + dot2).abs() / cross_len
767 }
768 }
769
770 pub fn closest_point_to(&self, p: [f64; 3]) -> [f64; 3] {
772 let q_on_line = [
775 self.direction[1] * self.moment[2] - self.direction[2] * self.moment[1],
776 self.direction[2] * self.moment[0] - self.direction[0] * self.moment[2],
777 self.direction[0] * self.moment[1] - self.direction[1] * self.moment[0],
778 ];
779 let ap = [
780 p[0] - q_on_line[0],
781 p[1] - q_on_line[1],
782 p[2] - q_on_line[2],
783 ];
784 let t = ap[0] * self.direction[0] + ap[1] * self.direction[1] + ap[2] * self.direction[2];
785 [
786 q_on_line[0] + t * self.direction[0],
787 q_on_line[1] + t * self.direction[1],
788 q_on_line[2] + t * self.direction[2],
789 ]
790 }
791
792 pub fn to_dual_quaternion_rotation(&self, angle: f64) -> DualQuaternion {
796 let axis_point = [
799 self.direction[1] * self.moment[2] - self.direction[2] * self.moment[1],
800 self.direction[2] * self.moment[0] - self.direction[0] * self.moment[2],
801 self.direction[0] * self.moment[1] - self.direction[1] * self.moment[0],
802 ];
803 DualQuaternion::from_twist(self.direction, angle, 0.0, axis_point)
804 }
805}
806
807#[derive(Debug, Clone, Copy)]
817pub struct VelocityScrew {
818 pub angular: [f64; 3],
820 pub linear: [f64; 3],
822}
823
824impl VelocityScrew {
825 pub fn zero() -> Self {
827 Self {
828 angular: [0.0; 3],
829 linear: [0.0; 3],
830 }
831 }
832
833 pub fn velocity_at_point(&self, p: [f64; 3]) -> [f64; 3] {
837 let w = self.angular;
838 let v = self.linear;
839 let cross = [
841 w[1] * p[2] - w[2] * p[1],
842 w[2] * p[0] - w[0] * p[2],
843 w[0] * p[1] - w[1] * p[0],
844 ];
845 [v[0] + cross[0], v[1] + cross[1], v[2] + cross[2]]
846 }
847
848 pub fn pitch(&self) -> f64 {
852 let w_sq = self.angular[0] * self.angular[0]
853 + self.angular[1] * self.angular[1]
854 + self.angular[2] * self.angular[2];
855 if w_sq < f64::EPSILON {
856 return 0.0;
857 }
858 let v_dot_w = self.linear[0] * self.angular[0]
859 + self.linear[1] * self.angular[1]
860 + self.linear[2] * self.angular[2];
861 v_dot_w / w_sq
862 }
863
864 pub fn axis_direction(&self) -> [f64; 3] {
866 let len = (self.angular[0] * self.angular[0]
867 + self.angular[1] * self.angular[1]
868 + self.angular[2] * self.angular[2])
869 .sqrt();
870 if len < f64::EPSILON {
871 [0.0, 0.0, 1.0]
872 } else {
873 [
874 self.angular[0] / len,
875 self.angular[1] / len,
876 self.angular[2] / len,
877 ]
878 }
879 }
880
881 pub fn add(&self, other: &VelocityScrew) -> VelocityScrew {
883 VelocityScrew {
884 angular: [
885 self.angular[0] + other.angular[0],
886 self.angular[1] + other.angular[1],
887 self.angular[2] + other.angular[2],
888 ],
889 linear: [
890 self.linear[0] + other.linear[0],
891 self.linear[1] + other.linear[1],
892 self.linear[2] + other.linear[2],
893 ],
894 }
895 }
896
897 pub fn scale(&self, s: f64) -> VelocityScrew {
899 VelocityScrew {
900 angular: [
901 self.angular[0] * s,
902 self.angular[1] * s,
903 self.angular[2] * s,
904 ],
905 linear: [self.linear[0] * s, self.linear[1] * s, self.linear[2] * s],
906 }
907 }
908
909 pub fn to_dual_quaternion_step(&self, dt: f64) -> DualQuaternion {
912 let angle = {
913 let w = self.angular;
914 (w[0] * w[0] + w[1] * w[1] + w[2] * w[2]).sqrt() * dt
915 };
916 let axis = self.axis_direction();
917 let pitch = self.pitch();
918 DualQuaternion::from_twist(axis, angle, pitch, [0.0; 3])
919 }
920}
921
922#[allow(clippy::too_many_arguments)]
943pub fn dh_transform(a: f64, alpha: f64, d: f64, theta: f64) -> DualQuaternion {
944 let q_theta = Quaternion::from_axis_angle([0.0, 0.0, 1.0], theta);
946 let dq_theta = DualQuaternion::from_rotation_translation(q_theta, [0.0, 0.0, d]);
947
948 let q_alpha = Quaternion::from_axis_angle([1.0, 0.0, 0.0], alpha);
950 let dq_alpha = DualQuaternion::from_rotation_translation(q_alpha, [a, 0.0, 0.0]);
951
952 dq_theta.mul(&dq_alpha)
953}
954
955pub fn dh_chain(params: &[(f64, f64, f64, f64)]) -> DualQuaternion {
960 params
961 .iter()
962 .fold(DualQuaternion::identity(), |acc, &(a, al, d, th)| {
963 acc.mul(&dh_transform(a, al, d, th))
964 })
965}
966
967pub fn rigid_body_pose_blend(poses: &[DualQuaternion], weights: &[f64]) -> DualQuaternion {
976 dual_quaternion_blend(poses, weights)
977}
978
979pub fn geodesic_interpolate(
983 start: &DualQuaternion,
984 end: &DualQuaternion,
985 t: f64,
986) -> DualQuaternion {
987 start.sclerp(end, t)
988}
989
990pub fn relative_transform(from: &DualQuaternion, to: &DualQuaternion) -> DualQuaternion {
992 from.invert().mul(to)
993}
994
995pub fn finite_difference_angular_velocity(
999 dq1: &DualQuaternion,
1000 dq2: &DualQuaternion,
1001 dt: f64,
1002) -> [f64; 3] {
1003 if dt.abs() < f64::EPSILON {
1004 return [0.0; 3];
1005 }
1006 let q1 = dq1.to_rotation();
1007 let q2 = dq2.to_rotation();
1008 let dq = q1.conjugate().mul(&q2);
1009 let log = dq.ln();
1010 let scale = 2.0 / dt;
1012 [log.x * scale, log.y * scale, log.z * scale]
1013}
1014
1015#[cfg(test)]
1020mod tests {
1021 use super::*;
1022 use std::f64::consts::PI;
1023
1024 fn approx_eq(a: f64, b: f64, eps: f64) -> bool {
1025 (a - b).abs() < eps
1026 }
1027
1028 fn vec3_approx_eq(a: [f64; 3], b: [f64; 3], eps: f64) -> bool {
1029 approx_eq(a[0], b[0], eps) && approx_eq(a[1], b[1], eps) && approx_eq(a[2], b[2], eps)
1030 }
1031
1032 fn quat_approx_eq(a: Quaternion, b: Quaternion, eps: f64) -> bool {
1033 let direct = approx_eq(a.w, b.w, eps)
1035 && approx_eq(a.x, b.x, eps)
1036 && approx_eq(a.y, b.y, eps)
1037 && approx_eq(a.z, b.z, eps);
1038 let negated = approx_eq(a.w, -b.w, eps)
1039 && approx_eq(a.x, -b.x, eps)
1040 && approx_eq(a.y, -b.y, eps)
1041 && approx_eq(a.z, -b.z, eps);
1042 direct || negated
1043 }
1044
1045 fn dq_approx_eq(a: DualQuaternion, b: DualQuaternion, eps: f64) -> bool {
1046 quat_approx_eq(a.real, b.real, eps) && quat_approx_eq(a.dual, b.dual, eps)
1047 }
1048
1049 #[test]
1052 fn test_rotate_z90() {
1053 let q = Quaternion::from_axis_angle([0.0, 0.0, 1.0], PI / 2.0);
1054 let v = q.rotate_vector([1.0, 0.0, 0.0]);
1055 assert!(vec3_approx_eq(v, [0.0, 1.0, 0.0], 1e-12));
1056 }
1057
1058 #[test]
1059 fn test_slerp_endpoints() {
1060 let q1 = Quaternion::from_axis_angle([0.0, 0.0, 1.0], 0.0);
1061 let q2 = Quaternion::from_axis_angle([0.0, 0.0, 1.0], PI / 2.0);
1062 let s0 = q1.slerp(&q2, 0.0);
1063 let s1 = q1.slerp(&q2, 1.0);
1064 assert!(quat_approx_eq(s0, q1, 1e-10));
1065 assert!(quat_approx_eq(s1, q2, 1e-10));
1066 }
1067
1068 #[test]
1069 fn test_to_rotation_matrix_orthogonal() {
1070 let q = Quaternion::from_axis_angle([1.0, 1.0, 0.0], PI / 3.0);
1071 let m = q.to_rotation_matrix();
1072 for i in 0..3 {
1074 for j in 0..3 {
1075 let dot: f64 = (0..3).map(|k| m[k][i] * m[k][j]).sum();
1076 let expected = if i == j { 1.0 } else { 0.0 };
1077 assert!(approx_eq(dot, expected, 1e-12), "R^T R [{i}][{j}] = {dot}");
1078 }
1079 }
1080 }
1081
1082 #[test]
1083 fn test_from_rotation_matrix_round_trip() {
1084 let q0 = Quaternion::from_axis_angle([0.0, 1.0, 0.0], PI / 4.0);
1085 let m = q0.to_rotation_matrix();
1086 let q1 = Quaternion::from_rotation_matrix(&m);
1087 assert!(quat_approx_eq(q0, q1, 1e-12));
1088 }
1089
1090 #[test]
1091 fn test_exp_ln_round_trip() {
1092 let q = Quaternion::new(0.0, 0.5, 0.3, 0.1); let recovered = q.exp().ln();
1094 assert!(quat_approx_eq(q, recovered, 1e-12));
1095 }
1096
1097 #[test]
1100 fn test_dual_number_mul() {
1101 let a = DualNumber::new(2.0, 3.0);
1102 let b = DualNumber::new(4.0, 5.0);
1103 let c = a.mul(&b);
1104 assert!(approx_eq(c.real, 8.0, 1e-15));
1105 assert!(approx_eq(c.dual, 2.0 * 5.0 + 3.0 * 4.0, 1e-15));
1106 }
1107
1108 #[test]
1111 fn test_identity_transforms_point() {
1112 let dq = DualQuaternion::identity();
1113 let p = [1.0, 2.0, 3.0];
1114 let tp = dq.transform_point(p);
1115 assert!(vec3_approx_eq(tp, p, 1e-12));
1116 }
1117
1118 #[test]
1119 fn test_from_rotation_translation_recovers_translation() {
1120 let q = Quaternion::identity();
1121 let t = [3.0, -1.0, 2.0];
1122 let dq = DualQuaternion::from_rotation_translation(q, t);
1123 let tr = dq.to_translation();
1124 assert!(vec3_approx_eq(tr, t, 1e-12), "got {tr:?}");
1125 }
1126
1127 #[test]
1128 fn test_pure_translation_transform() {
1129 let q = Quaternion::identity();
1130 let t = [5.0, 0.0, 0.0];
1131 let dq = DualQuaternion::from_rotation_translation(q, t);
1132 let p = [1.0, 2.0, 3.0];
1133 let tp = dq.transform_point(p);
1134 assert!(vec3_approx_eq(tp, [6.0, 2.0, 3.0], 1e-12), "got {tp:?}");
1135 }
1136
1137 #[test]
1138 fn test_mul_identity_left() {
1139 let id = DualQuaternion::identity();
1140 let q = Quaternion::from_axis_angle([0.0, 1.0, 0.0], PI / 3.0);
1141 let dq = DualQuaternion::from_rotation_translation(q, [1.0, 2.0, 3.0]);
1142 let result = id.mul(&dq);
1143 assert!(dq_approx_eq(result.normalize(), dq.normalize(), 1e-12));
1144 }
1145
1146 #[test]
1147 fn test_sclerp_endpoints() {
1148 let q1 = Quaternion::from_axis_angle([0.0, 0.0, 1.0], 0.0);
1149 let dq1 = DualQuaternion::from_rotation_translation(q1, [0.0, 0.0, 0.0]);
1150 let q2 = Quaternion::from_axis_angle([0.0, 0.0, 1.0], PI / 2.0);
1151 let dq2 = DualQuaternion::from_rotation_translation(q2, [1.0, 0.0, 0.0]);
1152
1153 let s0 = dq1.sclerp(&dq2, 0.0).normalize();
1154 let s1 = dq1.sclerp(&dq2, 1.0).normalize();
1155
1156 assert!(
1158 vec3_approx_eq(s0.to_translation(), dq1.to_translation(), 1e-8),
1159 "sclerp(0) translation: {:?}",
1160 s0.to_translation()
1161 );
1162 assert!(
1163 vec3_approx_eq(s1.to_translation(), dq2.to_translation(), 1e-8),
1164 "sclerp(1) translation: {:?}",
1165 s1.to_translation()
1166 );
1167 }
1168
1169 #[test]
1170 fn test_rotation_translation_composition() {
1171 let q = Quaternion::from_axis_angle([0.0, 0.0, 1.0], PI / 2.0);
1172 let t = [0.0, 0.0, 5.0];
1173 let dq = DualQuaternion::from_rotation_translation(q, t);
1174 let p = [1.0, 0.0, 0.0];
1175 let tp = dq.transform_point(p);
1176 assert!(vec3_approx_eq(tp, [0.0, 1.0, 5.0], 1e-12), "got {tp:?}");
1178 }
1179
1180 #[test]
1183 fn test_dlb_single_quaternion() {
1184 let q = Quaternion::from_axis_angle([0.0, 0.0, 1.0], PI / 4.0);
1185 let dq = DualQuaternion::from_rotation_translation(q, [1.0, 0.0, 0.0]);
1186 let blended = dual_quaternion_blend(&[dq], &[1.0]);
1187 let p = [0.0, 0.0, 0.0];
1189 let tp1 = dq.transform_point(p);
1190 let tp2 = blended.transform_point(p);
1191 assert!(
1192 vec3_approx_eq(tp1, tp2, 1e-10),
1193 "DLB of single DQ should be identity, got {:?}",
1194 tp2
1195 );
1196 }
1197
1198 #[test]
1199 fn test_dlb_two_equal_weights() {
1200 let q = Quaternion::from_axis_angle([0.0, 1.0, 0.0], PI / 6.0);
1202 let dq = DualQuaternion::from_rotation_translation(q, [0.0, 0.0, 0.0]);
1203 let blended = dual_quaternion_blend(&[dq, dq], &[0.5, 0.5]);
1204 let p = [1.0, 0.0, 0.0];
1205 let tp1 = dq.transform_point(p);
1206 let tp2 = blended.transform_point(p);
1207 assert!(
1208 vec3_approx_eq(tp1, tp2, 1e-10),
1209 "DLB of equal pair should equal original"
1210 );
1211 }
1212
1213 #[test]
1214 fn test_dlb_empty_returns_identity() {
1215 let blended = dual_quaternion_blend(&[], &[]);
1216 let id = DualQuaternion::identity();
1217 let p = [1.0, 2.0, 3.0];
1218 assert!(vec3_approx_eq(
1219 blended.transform_point(p),
1220 id.transform_point(p),
1221 1e-12
1222 ));
1223 }
1224
1225 #[test]
1228 fn test_nlerp_endpoints() {
1229 let q1 = Quaternion::identity();
1230 let dq1 = DualQuaternion::from_rotation_translation(q1, [0.0, 0.0, 0.0]);
1231 let q2 = Quaternion::from_axis_angle([0.0, 0.0, 1.0], PI / 2.0);
1232 let dq2 = DualQuaternion::from_rotation_translation(q2, [2.0, 0.0, 0.0]);
1233 let s0 = dq1.nlerp(&dq2, 0.0);
1234 let s1 = dq1.nlerp(&dq2, 1.0);
1235 let p = [0.0, 0.0, 0.0];
1236 assert!(
1237 vec3_approx_eq(s0.transform_point(p), dq1.transform_point(p), 1e-10),
1238 "nlerp t=0 should recover dq1"
1239 );
1240 assert!(
1241 vec3_approx_eq(s1.transform_point(p), dq2.transform_point(p), 1e-10),
1242 "nlerp t=1 should recover dq2"
1243 );
1244 }
1245
1246 #[test]
1249 fn test_compose_transforms() {
1250 let t = DualQuaternion::from_rotation_translation(Quaternion::identity(), [1.0, 0.0, 0.0]);
1252 let r = DualQuaternion::from_rotation_translation(
1253 Quaternion::from_axis_angle([0.0, 0.0, 1.0], PI / 2.0),
1254 [0.0, 0.0, 0.0],
1255 );
1256 let combined = t.compose_transforms(&r);
1257 let p = [0.0, 0.0, 0.0];
1258 let result = combined.transform_point(p);
1260 assert!(
1261 vec3_approx_eq(result, [0.0, 1.0, 0.0], 1e-10),
1262 "composed transform: expected [0,1,0], got {:?}",
1263 result
1264 );
1265 }
1266
1267 #[test]
1270 fn test_rotation_angle_pi_over_3() {
1271 let angle = PI / 3.0;
1272 let q = Quaternion::from_axis_angle([0.0, 1.0, 0.0], angle);
1273 let dq = DualQuaternion::from_rotation_translation(q, [0.0, 0.0, 0.0]);
1274 let extracted = dq.rotation_angle();
1275 assert!(
1276 approx_eq(extracted, angle, 1e-12),
1277 "Expected angle {angle}, got {extracted}"
1278 );
1279 }
1280
1281 #[test]
1282 fn test_rotation_axis_z() {
1283 let q = Quaternion::from_axis_angle([0.0, 0.0, 1.0], PI / 4.0);
1284 let dq = DualQuaternion::from_rotation_translation(q, [0.0, 0.0, 0.0]);
1285 let axis = dq.rotation_axis();
1286 assert!(
1287 vec3_approx_eq(axis, [0.0, 0.0, 1.0], 1e-10),
1288 "Axis should be [0,0,1], got {:?}",
1289 axis
1290 );
1291 }
1292
1293 #[test]
1294 fn test_rotation_axis_identity() {
1295 let dq = DualQuaternion::identity();
1296 let axis = dq.rotation_axis();
1297 assert!(vec3_approx_eq(axis, [0.0, 0.0, 1.0], 1e-10));
1299 }
1300
1301 #[test]
1304 fn test_sclerp_sequence_length() {
1305 let dq1 = DualQuaternion::identity();
1306 let q2 = Quaternion::from_axis_angle([0.0, 0.0, 1.0], PI / 2.0);
1307 let dq2 = DualQuaternion::from_rotation_translation(q2, [0.0, 0.0, 0.0]);
1308 let seq = sclerp_sequence(&dq1, &dq2, 5);
1309 assert_eq!(seq.len(), 5, "sequence should have 5 poses");
1310 }
1311
1312 #[test]
1313 fn test_sclerp_sequence_endpoints() {
1314 let dq1 = DualQuaternion::identity();
1315 let q2 = Quaternion::from_axis_angle([0.0, 0.0, 1.0], PI / 2.0);
1316 let dq2 = DualQuaternion::from_rotation_translation(q2, [1.0, 0.0, 0.0]);
1317 let seq = sclerp_sequence(&dq1, &dq2, 10);
1318 let p = [1.0, 0.0, 0.0];
1319 let t0 = seq[0].transform_point(p);
1320 let t9 = seq[9].transform_point(p);
1321 let expected0 = dq1.transform_point(p);
1322 let expected9 = dq2.transform_point(p);
1323 assert!(
1324 vec3_approx_eq(t0, expected0, 1e-8),
1325 "seq[0] should match start"
1326 );
1327 assert!(
1328 vec3_approx_eq(t9, expected9, 1e-8),
1329 "seq[n-1] should match end"
1330 );
1331 }
1332
1333 #[test]
1334 fn test_sclerp_sequence_empty() {
1335 let dq = DualQuaternion::identity();
1336 let seq = sclerp_sequence(&dq, &dq, 0);
1337 assert!(seq.is_empty());
1338 }
1339
1340 #[test]
1343 fn test_dq_invert_composition_is_identity() {
1344 let q = Quaternion::from_axis_angle([0.0, 1.0, 0.0], PI / 3.0);
1345 let dq = DualQuaternion::from_rotation_translation(q, [1.0, 2.0, 3.0]);
1346 let inv = dq.invert();
1347 let product = dq.mul(&inv).normalize();
1348 let id = DualQuaternion::identity();
1349 let p = [5.0, -2.0, 1.0];
1350 let tp1 = product.transform_point(p);
1351 let tp2 = id.transform_point(p);
1352 assert!(
1353 vec3_approx_eq(tp1, tp2, 1e-10),
1354 "dq * inv(dq) should be identity transform, got {:?}",
1355 tp1
1356 );
1357 }
1358
1359 #[test]
1362 fn test_transform_points_batch() {
1363 let q = Quaternion::identity();
1364 let dq = DualQuaternion::from_rotation_translation(q, [1.0, 0.0, 0.0]);
1365 let points = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1366 let transformed = dq.transform_points(&points);
1367 assert_eq!(transformed.len(), 3);
1368 for (orig, trans) in points.iter().zip(transformed.iter()) {
1370 assert!((trans[0] - orig[0] - 1.0).abs() < 1e-12);
1371 assert!((trans[1] - orig[1]).abs() < 1e-12);
1372 }
1373 }
1374
1375 #[test]
1378 fn test_plucker_from_points() {
1379 let p = [0.0, 0.0, 0.0];
1380 let q = [1.0, 0.0, 0.0];
1381 let pl = PluckerLine::from_two_points(p, q);
1382 assert!((pl.direction[0] - 1.0).abs() < 1e-12);
1384 assert!(pl.direction[1].abs() < 1e-12);
1385 assert!(pl.direction[2].abs() < 1e-12);
1386 }
1387
1388 #[test]
1389 fn test_plucker_moment_through_origin() {
1390 let p = [0.0, 0.0, 0.0];
1392 let q = [0.0, 1.0, 0.0];
1393 let pl = PluckerLine::from_two_points(p, q);
1394 let mom_sq = pl.moment[0].powi(2) + pl.moment[1].powi(2) + pl.moment[2].powi(2);
1395 assert!(mom_sq.sqrt() < 1e-12, "moment={:?}", pl.moment);
1396 }
1397
1398 #[test]
1399 fn test_plucker_distance_parallel_lines() {
1400 let l1 = PluckerLine::from_two_points([0.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
1402 let l2 = PluckerLine::from_two_points([0.0, 1.0, 0.0], [1.0, 1.0, 0.0]);
1403 let d = l1.distance_to_line(&l2);
1404 assert!((d - 1.0).abs() < 1e-10, "distance={}", d);
1405 }
1406
1407 #[test]
1410 fn test_dh_transform_zero_params() {
1411 let dq = dh_transform(0.0, 0.0, 0.0, 0.0);
1413 let p = [1.0, 0.0, 0.0];
1414 let tp = dq.transform_point(p);
1415 assert!(vec3_approx_eq(tp, p, 1e-10), "got {:?}", tp);
1416 }
1417
1418 #[test]
1419 fn test_dh_transform_pure_translation_d() {
1420 let dq = dh_transform(0.0, 0.0, 2.0, 0.0);
1422 let p = [0.0, 0.0, 0.0];
1423 let tp = dq.transform_point(p);
1424 assert!(vec3_approx_eq(tp, [0.0, 0.0, 2.0], 1e-10), "got {:?}", tp);
1425 }
1426
1427 #[test]
1428 fn test_dh_transform_theta_rotation() {
1429 let dq = dh_transform(0.0, 0.0, 0.0, PI / 2.0);
1431 let p = [1.0, 0.0, 0.0];
1432 let tp = dq.transform_point(p);
1433 assert!(vec3_approx_eq(tp, [0.0, 1.0, 0.0], 1e-10), "got {:?}", tp);
1434 }
1435
1436 #[test]
1439 fn test_velocity_screw_zero() {
1440 let vs = VelocityScrew::zero();
1441 let v = vs.velocity_at_point([1.0, 0.0, 0.0]);
1442 assert!(vec3_approx_eq(v, [0.0, 0.0, 0.0], 1e-12));
1443 }
1444
1445 #[test]
1446 fn test_velocity_screw_pure_translation() {
1447 let vs = VelocityScrew {
1448 angular: [0.0; 3],
1449 linear: [1.0, 0.0, 0.0],
1450 };
1451 let v = vs.velocity_at_point([0.0, 5.0, 0.0]);
1452 assert!(vec3_approx_eq(v, [1.0, 0.0, 0.0], 1e-12), "got {:?}", v);
1454 }
1455
1456 #[test]
1457 fn test_velocity_screw_pure_rotation_z() {
1458 let vs = VelocityScrew {
1460 angular: [0.0, 0.0, 1.0],
1461 linear: [0.0, 0.0, 0.0],
1462 };
1463 let v = vs.velocity_at_point([1.0, 0.0, 0.0]);
1464 assert!(vec3_approx_eq(v, [0.0, 1.0, 0.0], 1e-12), "got {:?}", v);
1465 }
1466
1467 #[test]
1468 fn test_velocity_screw_addition() {
1469 let a = VelocityScrew {
1470 angular: [1.0, 0.0, 0.0],
1471 linear: [0.0, 1.0, 0.0],
1472 };
1473 let b = VelocityScrew {
1474 angular: [0.0, 1.0, 0.0],
1475 linear: [0.0, 0.0, 1.0],
1476 };
1477 let s = a.add(&b);
1478 assert!(vec3_approx_eq(s.angular, [1.0, 1.0, 0.0], 1e-12));
1479 assert!(vec3_approx_eq(s.linear, [0.0, 1.0, 1.0], 1e-12));
1480 }
1481
1482 #[test]
1485 fn test_pose_blend_two_equal() {
1486 let q = Quaternion::from_axis_angle([0.0, 1.0, 0.0], PI / 4.0);
1487 let dq = DualQuaternion::from_rotation_translation(q, [1.0, 2.0, 3.0]);
1488 let blended = rigid_body_pose_blend(&[dq, dq], &[0.5, 0.5]);
1489 let p = [1.0, 0.0, 0.0];
1490 let tp1 = dq.transform_point(p);
1491 let tp2 = blended.transform_point(p);
1492 assert!(vec3_approx_eq(tp1, tp2, 1e-8), "blend of equal = original");
1493 }
1494
1495 #[test]
1496 fn test_pose_blend_weighted_translation() {
1497 let dq1 =
1498 DualQuaternion::from_rotation_translation(Quaternion::identity(), [0.0, 0.0, 0.0]);
1499 let dq2 =
1500 DualQuaternion::from_rotation_translation(Quaternion::identity(), [2.0, 0.0, 0.0]);
1501 let blended = rigid_body_pose_blend(&[dq1, dq2], &[0.5, 0.5]);
1503 let t = blended.to_translation();
1504 assert!((t[0] - 1.0).abs() < 1e-8, "t[0]={}", t[0]);
1505 }
1506
1507 #[test]
1510 fn test_sclerp_sequence_midpoint() {
1511 let dq1 = DualQuaternion::identity();
1512 let q2 = Quaternion::from_axis_angle([0.0, 0.0, 1.0], PI / 2.0);
1513 let dq2 = DualQuaternion::from_rotation_translation(q2, [0.0, 0.0, 0.0]);
1514 let seq = sclerp_sequence(&dq1, &dq2, 3);
1515 let angle = seq[1].rotation_angle();
1517 assert!((angle - PI / 4.0).abs() < 1e-6, "midpoint angle={}", angle);
1518 }
1519
1520 #[test]
1521 fn test_dq_mul_sequence() {
1522 let q = Quaternion::from_axis_angle([0.0, 0.0, 1.0], PI / 8.0);
1524 let dq = DualQuaternion::from_rotation_translation(q, [0.0, 0.0, 0.0]);
1525 let dq4 = dq.mul(&dq).mul(&dq).mul(&dq);
1526 let angle = dq4.rotation_angle();
1527 assert!(
1528 (angle - PI / 2.0).abs() < 1e-10,
1529 "4x rotation angle={}",
1530 angle
1531 );
1532 }
1533
1534 #[test]
1537 fn test_from_twist_pure_rotation_origin() {
1538 let dq = DualQuaternion::from_twist([0.0, 0.0, 1.0], PI / 2.0, 0.0, [0.0, 0.0, 0.0]);
1541 let result = dq.transform_point([1.0, 0.0, 0.0]);
1542 assert!(
1543 vec3_approx_eq(result, [0.0, 1.0, 0.0], 1e-10),
1544 "got {:?}",
1545 result
1546 );
1547 }
1548
1549 #[test]
1550 fn test_from_twist_pure_translation_z() {
1551 let dq = DualQuaternion::from_twist([0.0, 0.0, 1.0], 0.0, 1.0, [0.0, 0.0, 0.0]);
1553 let result = dq.transform_point([0.0, 0.0, 0.0]);
1554 assert!(vec3_approx_eq(result, [0.0, 0.0, 0.0], 1e-10));
1556 }
1557
1558 #[test]
1559 fn test_from_twist_screw_with_pitch() {
1560 let dq = DualQuaternion::from_twist([0.0, 0.0, 1.0], PI / 2.0, 1.0, [0.0, 0.0, 0.0]);
1562 let t = dq.to_translation();
1563 assert!((t[2] - PI / 2.0).abs() < 1e-10, "t_z={}", t[2]);
1565 assert!(t[0].abs() < 1e-10 && t[1].abs() < 1e-10);
1567 }
1568
1569 #[test]
1570 fn test_from_twist_off_origin_axis() {
1571 let dq = DualQuaternion::from_twist([0.0, 0.0, 1.0], PI / 2.0, 0.0, [1.0, 0.0, 0.0]);
1575 let result = dq.transform_point([0.0, 0.0, 0.0]);
1576 assert!(
1577 vec3_approx_eq(result, [1.0, -1.0, 0.0], 1e-10),
1578 "got {:?}",
1579 result
1580 );
1581 }
1582
1583 #[test]
1584 fn test_from_twist_transform_point_roundtrip() {
1585 let dq = DualQuaternion::from_twist([0.0, 0.0, 1.0], PI / 2.0, 0.0, [0.0, 0.0, 0.0]);
1590 let p = dq.transform_point([1.0, 0.0, 0.0]);
1592 assert!(vec3_approx_eq(p, [0.0, 1.0, 0.0], 1e-10), "got {:?}", p);
1593 let q_pt = dq.transform_point([0.0, 1.0, 0.0]);
1595 assert!(
1596 vec3_approx_eq(q_pt, [-1.0, 0.0, 0.0], 1e-10),
1597 "got {:?}",
1598 q_pt
1599 );
1600 }
1601}