1use geop_core_math::{
19 geop_error::{GeopError, GeopResult},
20 plain::{cross, dot, sub},
21 primitives::Pose,
22 scalars::{Number, Scalar, as_f64},
23 vector::Vector3,
24};
25use geop_ops_model::selection::Shape;
26use serde::{Deserialize, Serialize};
27use std::collections::BTreeMap;
28
29use crate::residuals::{D, Placing, parallel};
30
31#[derive(Clone, Copy, Debug, PartialEq)]
36pub struct Connector<T: Number = Scalar> {
37 pub origin: Vector3<T>,
38 pub axis: Vector3<T>,
39 pub reference: Vector3<T>,
40}
41
42impl Connector {
43 pub fn new(origin: Vector3, axis: Vector3, reference: Option<Vector3>) -> GeopResult<Self> {
50 let axis = axis
51 .normalize()
52 .map_err(|e| e.with_context(format!("the axis {axis:?} of a joint")))?;
53 let reference = match reference {
54 None => axis.across()?[0],
55 Some(reference) => {
56 let square = reference.sub(&axis.prod_scalar(axis.prod_dot(&reference)));
57 square.normalize().map_err(|e| {
58 e.with_context(format!(
59 "the reference {reference:?} of a joint runs along its axis {axis:?}"
60 ))
61 })?
62 }
63 };
64 Ok(Connector {
65 origin,
66 axis,
67 reference,
68 })
69 }
70
71 pub fn of(aspects: &Shape, face: bool) -> Option<GeopResult<Self>> {
77 if let Some(arc) = &aspects.arc {
78 return Some(Connector::new(arc.circle.center, arc.circle.normal, None));
79 }
80 if let Some(frame) = &aspects.frame {
81 return Some(Connector::new(
82 *frame.origin(),
83 *frame.w(),
84 Some(*frame.u()),
85 ));
86 }
87 if let (Some(round), false) = (&aspects.round, face) {
88 return Some(Connector::new(round.point, round.direction, None));
89 }
90 None
91 }
92
93 pub fn placed(&self, pose: Option<&Pose<D>>) -> Connector<D> {
96 let placing = Placing::new(pose);
97 let turn = |d: &Vector3| placing.turn(&d.map(D::cst));
98 Connector {
99 origin: placing.point(&self.origin),
100 axis: turn(&self.axis),
101 reference: turn(&self.reference),
102 }
103 }
104}
105
106#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
111pub enum Motion {
112 #[serde(rename = "angle")]
113 Turn,
114 #[serde(rename = "distance")]
115 Slide,
116 #[serde(rename = "x")]
117 SlideX,
118 #[serde(rename = "y")]
119 SlideY,
120}
121
122pub const MOTIONS: usize = 4;
124
125impl Motion {
126 pub const ALL: [Motion; MOTIONS] =
127 [Motion::Turn, Motion::Slide, Motion::SlideX, Motion::SlideY];
128
129 pub fn index(self) -> usize {
132 self as usize
133 }
134
135 pub fn name(self) -> &'static str {
138 match self {
139 Motion::Turn => "angle",
140 Motion::Slide => "distance",
141 Motion::SlideX => "x",
142 Motion::SlideY => "y",
143 }
144 }
145
146 pub fn measure(self) -> crate::solving::Measure {
148 match self {
149 Motion::Turn => crate::solving::Measure::Angle,
150 Motion::Slide | Motion::SlideX | Motion::SlideY => crate::solving::Measure::Distance,
151 }
152 }
153
154 pub fn verb(self) -> &'static str {
156 match self {
157 Motion::Turn => "turn",
158 Motion::Slide | Motion::SlideX | Motion::SlideY => "slide",
159 }
160 }
161
162 pub fn natural(self, value: D) -> GeopResult<D> {
165 match self {
166 Motion::Turn => value.degrees_to_radians(),
167 Motion::Slide | Motion::SlideX | Motion::SlideY => Ok(value),
168 }
169 }
170}
171
172#[derive(Clone, Copy, Debug, PartialEq, Eq)]
174pub enum Turning {
175 Not,
177 AboutZ,
179 Cross,
185 Free,
188}
189
190#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
195#[serde(rename_all = "snake_case")]
196pub enum Joint {
197 Revolute,
199 Slider,
201 Cylindrical,
203 Planar,
206 PinSlot,
209 Ball,
211 Universal,
214 Fastened,
216}
217
218impl Joint {
219 pub const ALL: [Joint; 8] = [
221 Joint::Revolute,
222 Joint::Slider,
223 Joint::Cylindrical,
224 Joint::Planar,
225 Joint::PinSlot,
226 Joint::Ball,
227 Joint::Universal,
228 Joint::Fastened,
229 ];
230
231 pub fn frees(self) -> (Turning, &'static [Motion]) {
235 use Motion::*;
236 match self {
237 Joint::Revolute => (Turning::AboutZ, &[Turn]),
238 Joint::Slider => (Turning::Not, &[Slide]),
239 Joint::Cylindrical => (Turning::AboutZ, &[Turn, Slide]),
240 Joint::Planar => (Turning::AboutZ, &[Turn, SlideX, SlideY]),
241 Joint::PinSlot => (Turning::AboutZ, &[Turn, SlideX]),
242 Joint::Ball => (Turning::Free, &[]),
243 Joint::Universal => (Turning::Cross, &[]),
244 Joint::Fastened => (Turning::Not, &[]),
245 }
246 }
247
248 pub fn name(self) -> &'static str {
250 match self {
251 Joint::Revolute => "revolute",
252 Joint::Slider => "slider",
253 Joint::Cylindrical => "cylindrical",
254 Joint::Planar => "planar",
255 Joint::PinSlot => "pin_slot",
256 Joint::Ball => "ball",
257 Joint::Universal => "universal",
258 Joint::Fastened => "fastened",
259 }
260 }
261
262 pub fn label(self) -> &'static str {
263 match self {
264 Joint::Revolute => "Revolute",
265 Joint::Slider => "Slider",
266 Joint::Cylindrical => "Cylindrical",
267 Joint::Planar => "Planar",
268 Joint::PinSlot => "Pin in slot",
269 Joint::Ball => "Ball",
270 Joint::Universal => "Universal",
271 Joint::Fastened => "Fastened",
272 }
273 }
274
275 pub fn doc(self) -> &'static str {
277 match self {
278 Joint::Revolute => {
279 "The second part turns about the first one's frame's z axis, and nothing else — a hinge: pick a frame on each part."
280 }
281 Joint::Slider => {
282 "The second part slides along the first one's frame's z axis, and nothing else: pick a frame on each part."
283 }
284 Joint::Cylindrical => {
285 "The second part turns about the first one's frame's z axis and slides along it — a shaft in a bearing."
286 }
287 Joint::Planar => {
288 "The second part slides in the plane of the first one's frame — along its x and y — and turns about its z: a part on a table."
289 }
290 Joint::PinSlot => {
291 "The second part slides along the first one's frame's x axis, the red arrow, and turns about its z: a pin in a straight slot."
292 }
293 Joint::Ball => {
294 "The second part turns any way about the first one's frame's origin, the two origins one: a ball in its socket."
295 }
296 Joint::Universal => {
297 "The second part turns about the first one's frame's z axis and about its own, the two kept square and meeting: a universal joint's cross."
298 }
299 Joint::Fastened => {
300 "The two parts are held together, their frames one: pick a frame on each part."
301 }
302 }
303 }
304}
305
306#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
309pub struct Limits {
310 #[serde(
311 default,
312 with = "as_f64::option",
313 skip_serializing_if = "Option::is_none"
314 )]
315 pub min: Option<Scalar>,
316 #[serde(
317 default,
318 with = "as_f64::option",
319 skip_serializing_if = "Option::is_none"
320 )]
321 pub max: Option<Scalar>,
322}
323
324impl Limits {
325 pub const NONE: Limits = Limits {
327 min: None,
328 max: None,
329 };
330
331 pub fn between(min: Scalar, max: Scalar) -> Self {
333 Limits {
334 min: Some(min),
335 max: Some(max),
336 }
337 }
338
339 pub fn is_none(&self) -> bool {
340 self.min.is_none() && self.max.is_none()
341 }
342}
343
344#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
349#[serde(from = "JointKindFile", into = "JointKindFile")]
350pub struct JointKind {
351 pub joint: Joint,
352 pub limits: [Limits; MOTIONS],
354}
355
356#[derive(Serialize, Deserialize)]
358struct JointKindFile {
359 #[serde(rename = "type")]
360 joint: Joint,
361 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
362 limits: BTreeMap<Motion, Limits>,
363}
364
365impl From<JointKindFile> for JointKind {
366 fn from(file: JointKindFile) -> Self {
367 let mut kind = JointKind::new(file.joint);
368 for (motion, limits) in file.limits {
369 kind.limits[motion.index()] = limits;
370 }
371 kind
372 }
373}
374
375impl From<JointKind> for JointKindFile {
376 fn from(kind: JointKind) -> Self {
377 JointKindFile {
378 joint: kind.joint,
379 limits: (Motion::ALL.into_iter())
380 .map(|m| (m, kind.limits[m.index()]))
381 .filter(|(_, limits)| !limits.is_none())
382 .collect(),
383 }
384 }
385}
386
387impl JointKind {
388 pub const fn new(joint: Joint) -> Self {
390 JointKind {
391 joint,
392 limits: [Limits::NONE; MOTIONS],
393 }
394 }
395
396 pub fn limited(mut self, motion: Motion, limits: Limits) -> Self {
398 self.limits[motion.index()] = limits;
399 self
400 }
401
402 pub fn turning(&self) -> Turning {
404 self.joint.frees().0
405 }
406
407 pub fn moves(&self, motion: Motion) -> bool {
409 self.joint.frees().1.contains(&motion)
410 }
411
412 pub fn motions(&self) -> Vec<Motion> {
414 self.joint.frees().1.to_vec()
415 }
416
417 pub fn limits(&self, motion: Motion) -> [Option<Scalar>; 2] {
419 let Limits { min, max } = self.limits[motion.index()];
420 [min, max]
421 }
422
423 pub fn label(&self) -> &'static str {
424 self.joint.label()
425 }
426
427 pub fn validate(&self) -> GeopResult<()> {
430 let label = self.label().to_lowercase();
431 for motion in Motion::ALL {
432 let [min, max] = self.limits(motion);
433 if !self.moves(motion) && (min.is_some() || max.is_some()) {
434 return Err(GeopError::new(format!(
435 "a {label} joint has no {} to limit: it frees {}",
436 motion.name(),
437 describe_motions(&self.motions()),
438 )));
439 }
440 if let (Some(min), Some(max)) = (min, max)
441 && min.definitely_greater(max)
442 {
443 return Err(GeopError::new(format!(
444 "the {} limits of a {label} joint are the wrong way round: {min:?} > {max:?}",
445 motion.name(),
446 )));
447 }
448 }
449 Ok(())
450 }
451}
452
453fn describe_motions(motions: &[Motion]) -> String {
455 match motions {
456 [] => "no coordinate".to_string(),
457 [one] => one.name().to_string(),
458 [rest @ .., last] => {
459 let rest: Vec<&str> = rest.iter().map(|m| m.name()).collect();
460 format!("{} and {}", rest.join(", "), last.name())
461 }
462 }
463}
464
465pub type Coordinates = [D; MOTIONS];
468
469pub fn joint_residuals(
482 kind: &JointKind,
483 a: &Connector<D>,
484 b: &Connector<D>,
485 at: &Coordinates,
486 scale: D,
487) -> GeopResult<Vec<D>> {
488 let (za, ra) = (&a.axis, &a.reference);
489 let l = scale;
490 let ya = za.prod_cross(ra);
491 let mut origin = a.origin.add(&za.prod_scalar(at[Motion::Slide.index()]));
492 for (motion, along) in [(Motion::SlideX, ra), (Motion::SlideY, &ya)] {
495 if kind.moves(motion) {
496 origin = origin.add(&along.prod_scalar(at[motion.index()]));
497 }
498 }
499 let mut out: Vec<D> = b.origin.sub(&origin).to_array().to_vec();
500 match kind.turning() {
501 Turning::Not | Turning::AboutZ => {
502 out.extend(parallel(&[*ra, ya], &b.axis, l));
505 let angle = Motion::Turn.natural(at[Motion::Turn.index()])?;
515 let (cos, sin) = (angle.cos(), angle.sin());
516 let u = ra.prod_scalar(cos).add(&ya.prod_scalar(sin));
517 let t = ya.prod_scalar(cos).sub(&ra.prod_scalar(sin));
518 let past = b
519 .reference
520 .prod_dot(&t)
521 .div(D::ONE.add(b.reference.prod_dot(&u)))?;
522 out.push(past.mul(D::TWO).mul(l));
523 }
524 Turning::Cross => out.push(za.prod_dot(&b.axis).mul(l)),
526 Turning::Free => {}
527 }
528 Ok(out)
529}
530
531pub fn measure(a: &Connector<D>, b: &Connector<D>) -> [f64; MOTIONS] {
538 let f = |v: &Vector3<D>| v.to_array().map(|x| x.to_f64());
539 let (oa, za, ra, ob, rb) = (
540 f(&a.origin),
541 f(&a.axis),
542 f(&a.reference),
543 f(&b.origin),
544 f(&b.reference),
545 );
546 let ya = cross(za, ra);
547 let off = sub(ob, oa);
548 let angle = dot(rb, ya).atan2(dot(rb, ra)).to_degrees();
549 [angle, dot(off, za), dot(off, ra), dot(off, ya)]
550}
551
552#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
556#[serde(rename_all = "snake_case")]
557pub enum Transmission {
558 Gear,
562 Belt,
565 RackPinion,
569 Screw,
573 Linear,
576}
577
578impl Transmission {
579 pub const ALL: [Transmission; 5] = [
581 Transmission::Gear,
582 Transmission::Belt,
583 Transmission::RackPinion,
584 Transmission::Screw,
585 Transmission::Linear,
586 ];
587
588 pub fn motions(self) -> [Motion; 2] {
590 match self {
591 Transmission::Gear | Transmission::Belt => [Motion::Turn, Motion::Turn],
592 Transmission::RackPinion | Transmission::Screw => [Motion::Turn, Motion::Slide],
593 Transmission::Linear => [Motion::Slide, Motion::Slide],
594 }
595 }
596
597 pub fn label(self) -> &'static str {
598 match self {
599 Transmission::Gear => "Gear",
600 Transmission::Belt => "Belt or chain",
601 Transmission::RackPinion => "Rack and pinion",
602 Transmission::Screw => "Screw",
603 Transmission::Linear => "Linear",
604 }
605 }
606}
607
608#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
614pub struct CouplingKind {
615 #[serde(rename = "type")]
616 pub transmission: Transmission,
617 #[serde(with = "as_f64")]
618 pub value: Scalar,
619 #[serde(default)]
620 pub reverse: bool,
621 #[serde(
622 default,
623 with = "as_f64::option",
624 skip_serializing_if = "Option::is_none"
625 )]
626 pub offset: Option<Scalar>,
627}
628
629impl CouplingKind {
630 pub fn new(transmission: Transmission, value: Scalar, reverse: bool) -> Self {
633 CouplingKind {
634 transmission,
635 value,
636 reverse,
637 offset: None,
638 }
639 }
640
641 pub fn factor(&self) -> GeopResult<Scalar> {
644 let factor = match self.transmission {
645 Transmission::Gear | Transmission::Belt => Scalar::ONE.div(self.value)?,
646 Transmission::RackPinion | Transmission::Linear => self.value,
647 Transmission::Screw => self.value.div(Scalar::TWO.mul(Scalar::PI))?,
648 };
649 Ok(if self.reverse { factor.neg() } else { factor })
650 }
651
652 pub fn validate(&self, same: bool, frees: [bool; 2]) -> GeopResult<()> {
656 let label = self.transmission.label().to_lowercase();
657 let [ma, mb] = self.transmission.motions();
658 if same && ma == mb {
659 return Err(GeopError::new(format!(
660 "a {label} coupling ties a joint's {} to itself",
661 ma.name()
662 )));
663 }
664 for (k, motion) in [ma, mb].into_iter().enumerate() {
665 if !frees[k] {
666 return Err(GeopError::new(format!(
667 "a {label} coupling needs its {} joint to {}",
668 ["first", "second"][k],
669 motion.verb()
670 )));
671 }
672 }
673 let ratio = matches!(self.transmission, Transmission::Gear | Transmission::Belt);
674 if ratio && self.value.could_be_equal(Scalar::ZERO) {
675 return Err(GeopError::new(format!(
676 "a {label} coupling's ratio must not be zero: {:?}",
677 self.value
678 )));
679 }
680 Ok(())
681 }
682
683 pub fn residual(&self, a: D, b: D, scale: D) -> GeopResult<D> {
688 let [ma, mb] = self.transmission.motions();
689 let mut off = mb
690 .natural(b)?
691 .sub(ma.natural(a)?.mul(D::cst(self.factor()?)));
692 if let Some(offset) = self.offset {
694 off = off.sub(mb.natural(D::cst(offset))?);
695 }
696 Ok(match mb {
697 Motion::Turn => off.mul(scale),
698 Motion::Slide | Motion::SlideX | Motion::SlideY => off,
699 })
700 }
701}