1use geop_core_geometry::{nurb_curve::NurbCurve3D, nurb_surface::NurbSurface3D};
17use geop_core_math::{
18 geop_error::{GeopError, GeopResult, WithContext},
19 primitives::{Motion, Pose},
20 scalars::{Number, Scalar, as_f64},
21 vector::{Vector, Vector3},
22 with_context,
23};
24use geop_ops_model::selection::{Role, Shape};
25use serde::{Deserialize, Serialize};
26
27pub use crate::solving::D;
28
29pub(crate) struct Placing(Option<Motion<D>>);
32
33impl Placing {
34 pub(crate) fn new(pose: Option<&Pose<D>>) -> Self {
35 Placing(pose.map(Pose::motion))
36 }
37
38 pub(crate) fn point(&self, at: &Vector3) -> Vector3<D> {
40 self.moved(&at.map(D::cst))
41 }
42
43 pub(crate) fn moved(&self, at: &Vector3<D>) -> Vector3<D> {
46 match &self.0 {
47 Some(m) => m.apply(at),
48 None => *at,
49 }
50 }
51
52 pub(crate) fn turn(&self, d: &Vector3<D>) -> Vector3<D> {
54 match &self.0 {
55 Some(m) => m.rotate(d),
56 None => *d,
57 }
58 }
59}
60
61pub(crate) fn parallel(across: &[Vector3<D>; 2], along: &Vector3<D>, scale: D) -> [D; 2] {
71 across.map(|x| x.prod_dot(along).mul(scale))
72}
73
74#[derive(Clone, Debug, PartialEq)]
77pub enum Geometry {
78 Point {
79 at: Vector3,
80 },
81 Line {
83 point: Vector3,
84 direction: Vector3,
85 },
86 Plane {
88 point: Vector3,
89 normal: Vector3,
90 },
91 Rim {
94 center: Vector3,
95 radius: Scalar,
96 },
97 Curve {
99 curve: NurbCurve3D,
100 },
101 Surface {
103 surface: NurbSurface3D,
104 },
105}
106
107impl Geometry {
108 fn kind(&self) -> &'static str {
109 match self {
110 Geometry::Point { .. } => "a point",
111 Geometry::Line { .. } => "a line",
112 Geometry::Plane { .. } => "a plane",
113 Geometry::Rim { .. } => "a circle",
114 Geometry::Curve { .. } => "a curve",
115 Geometry::Surface { .. } => "a face",
116 }
117 }
118
119 fn as_role(role: Role, shape: &Shape) -> Option<Geometry> {
122 let line = |a: &geop_core_geometry::shape::Axis| Geometry::Line {
123 point: a.point,
124 direction: a.direction,
125 };
126 match role {
127 Role::Point => (shape.point.as_ref())
128 .or(shape.arc.as_ref().map(|a| &a.circle.center))
129 .map(|at| Geometry::Point { at: *at }),
130 Role::Line => shape.line.as_ref().map(line),
131 Role::Round => shape.round.as_ref().map(line),
132 Role::Plane => shape.plane.as_ref().map(|p| Geometry::Plane {
133 point: *p.origin(),
134 normal: *p.w(),
135 }),
136 _ => None,
137 }
138 }
139}
140
141#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
143#[serde(tag = "type", rename_all = "snake_case")]
144pub enum Kind {
145 Coincident,
149 Concentric,
151 Parallel,
154 Perpendicular,
157 Distance {
160 #[serde(with = "as_f64")]
161 value: Scalar,
162 },
163 Angle {
165 #[serde(with = "as_f64")]
166 value: Scalar,
167 },
168 Tangent,
173 Path,
176}
177
178impl Kind {
179 pub fn label(&self) -> &'static str {
180 match self {
181 Kind::Coincident => "Coincident",
182 Kind::Concentric => "Concentric",
183 Kind::Parallel => "Parallel",
184 Kind::Perpendicular => "Perpendicular",
185 Kind::Distance { .. } => "Distance",
186 Kind::Angle { .. } => "Angle",
187 Kind::Tangent => "Tangent",
188 Kind::Path => "Path",
189 }
190 }
191
192 pub fn feet(&self) -> &'static [&'static str] {
197 match self {
198 Kind::Path => &["t"],
199 Kind::Tangent => &["u", "v"],
200 _ => &[],
201 }
202 }
203
204 pub fn roles(&self) -> &'static [Role] {
215 match self {
216 Kind::Coincident | Kind::Distance { .. } => {
217 &[Role::Point, Role::Line, Role::Plane, Role::Round]
218 }
219 Kind::Concentric => &[Role::Round, Role::Line],
220 Kind::Parallel | Kind::Perpendicular | Kind::Angle { .. } => {
221 &[Role::Line, Role::Plane, Role::Round]
222 }
223 Kind::Tangent => &[Role::Circle, Role::Point],
224 Kind::Path => &[Role::Curve, Role::Circle, Role::Point],
225 }
226 }
227
228 pub fn geometry(
236 &self,
237 k: usize,
238 shape: &Shape,
239 surface: impl FnOnce() -> GeopResult<Option<NurbSurface3D>>,
240 curve: impl FnOnce() -> GeopResult<Option<NurbCurve3D>>,
241 ) -> GeopResult<Option<Geometry>> {
242 Ok(match (self, k) {
243 (Kind::Tangent, 0) => surface()?.map(|surface| Geometry::Surface { surface }),
244 (Kind::Tangent, _) => (shape.arc.as_ref())
245 .map(|arc| Geometry::Rim {
246 center: arc.circle.center,
247 radius: arc.circle.radius,
248 })
249 .or_else(|| Geometry::as_role(Role::Point, shape)),
250 (Kind::Path, 0) => curve()?.map(|curve| Geometry::Curve { curve }),
251 (Kind::Path, _) => Geometry::as_role(Role::Point, shape),
252 _ => (self.roles().iter()).find_map(|&role| Geometry::as_role(role, shape)),
253 })
254 }
255
256 pub fn needs(&self, k: usize) -> Option<&'static str> {
260 match (self, k) {
261 (Kind::Tangent, 0) => Some("a face"),
262 (Kind::Tangent, _) => Some("a circle, an arc or a point"),
263 (Kind::Path, 0) => Some("an edge or a curve"),
264 (Kind::Path, _) => Some("a point or a circle"),
265 _ => None,
266 }
267 }
268
269 pub fn fits(&self, a: &Geometry, b: &Geometry) -> GeopResult<()> {
272 use Geometry::*;
273 let directed = |g: &Geometry| !matches!(g, Point { .. });
274 let flat = |g: &Geometry| matches!(g, Point { .. } | Line { .. } | Plane { .. });
275 let fits = match self {
276 Kind::Tangent => matches!(a, Surface { .. }) && matches!(b, Point { .. } | Rim { .. }),
277 Kind::Path => matches!(a, Curve { .. }) && matches!(b, Point { .. }),
278 _ if !(flat(a) && flat(b)) => false,
279 Kind::Coincident | Kind::Distance { .. } => true,
280 Kind::Concentric => matches!((a, b), (Line { .. }, Line { .. })),
281 Kind::Parallel | Kind::Perpendicular | Kind::Angle { .. } => directed(a) && directed(b),
282 };
283 let (a, b) = (a.kind(), b.kind());
284 match fits {
285 true => Ok(()),
286 false => Err(GeopError::new(format!(
287 "a {} constraint cannot hold between {} and {}",
288 self.label().to_lowercase(),
289 a,
290 b
291 ))),
292 }
293 }
294}
295
296pub struct Direction {
299 along: Vector3<D>,
300 across: [Vector3<D>; 2],
301}
302
303pub enum World {
305 Point(Vector3<D>),
306 Line(Vector3<D>, Direction),
307 Plane(Vector3<D>, Direction),
308}
309
310impl World {
311 fn direction(&self) -> Option<&Direction> {
313 match self {
314 World::Point(_) => None,
315 World::Line(_, d) | World::Plane(_, d) => Some(d),
316 }
317 }
318}
319
320impl Geometry {
321 pub fn placed(&self, pose: Option<&Pose<D>>) -> GeopResult<World> {
325 let placing = Placing::new(pose);
326 let direction = |d: &Vector3| -> GeopResult<Direction> {
329 let d = d.normalize()?;
330 Ok(Direction {
331 along: placing.turn(&d.map(D::cst)),
332 across: d.across()?.map(|v| placing.turn(&v.map(D::cst))),
333 })
334 };
335 Ok(match self {
336 Geometry::Point { at } => World::Point(placing.point(at)),
337 Geometry::Line {
338 point: p,
339 direction: d,
340 } => World::Line(placing.point(p), direction(d)?),
341 Geometry::Plane {
342 point: p,
343 normal: n,
344 } => World::Plane(placing.point(p), direction(n)?),
345 Geometry::Rim { center, .. } => World::Point(placing.point(center)),
346 Geometry::Curve { .. } | Geometry::Surface { .. } => {
347 return Err(GeopError::new(format!(
348 "{} is placed by where on it its foot point is",
349 self.kind()
350 )));
351 }
352 })
353 }
354
355 fn follower(&self, pose: Option<&Pose<D>>) -> GeopResult<Vector3<D>> {
358 match self {
359 Geometry::Point { at } | Geometry::Rim { center: at, .. } => {
360 Ok(Placing::new(pose).point(at))
361 }
362 _ => Err(GeopError::new(format!("{} follows nothing", self.kind()))),
363 }
364 }
365
366 fn radius(&self) -> Option<Scalar> {
368 match self {
369 Geometry::Rim { radius, .. } => Some(*radius),
370 _ => None,
371 }
372 }
373
374 fn foot(&self, feet: &[D], offset: Option<Scalar>) -> GeopResult<Vector3<D>> {
382 let domains = self.parameters();
383 if domains.len() != feet.len() {
384 return Err(GeopError::new(format!(
385 "{} has no foot point at {} parameters",
386 self.kind(),
387 feet.len()
388 )));
389 }
390 let (at, steps): (Vec<Scalar>, Vec<D>) = (feet.iter().zip(&domains))
394 .map(|(x, foot)| {
395 let x = foot.round(*x);
396 let at = x.v.clamped(foot.lo, foot.hi);
397 (at, x.sub(D::cst(at)))
398 })
399 .unzip();
400 match self {
401 Geometry::Curve { curve } => {
402 let (p, dp) = (curve.evaluate(at[0])?, curve.tangent(at[0])?);
403 Ok(first_order(&p, &[(&dp, &steps[0])]))
404 }
405 Geometry::Surface { surface } => {
406 let (du, dv) = (&steps[0], &steps[1]);
407 let order = if offset.is_some() { 2 } else { 1 };
408 let s = surface.partials(at[0], at[1], order)?;
409 let point = first_order(&s[0][0], &[(&s[1][0], du), (&s[0][1], dv)]);
410 let Some(offset) = offset else {
411 return Ok(point);
412 };
413 let su = first_order(&s[1][0], &[(&s[2][0], du), (&s[1][1], dv)]);
416 let sv = first_order(&s[0][1], &[(&s[1][1], du), (&s[0][2], dv)]);
417 let normal = su.prod_cross(&sv).normalize()?;
418 Ok(point.add(&normal.prod_scalar(D::cst(offset))))
419 }
420 _ => unreachable!("only a curve or a surface has feet"),
421 }
422 }
423
424 pub fn parameters(&self) -> Vec<Foot> {
427 let ends = |a: &[Vector<4>], b: &[Vector<4>]| {
430 a.len() == b.len() && a.iter().zip(b).all(|(p, q)| p.could_be_equal(q))
431 };
432 let foot = |(lo, hi): (Scalar, Scalar), closes| Foot { lo, hi, closes };
433 match self {
434 Geometry::Curve { curve } => {
435 let points = &curve.control_points;
436 let closes = ends(&points[..1], &points[points.len() - 1..]);
437 vec![foot(curve.domain(), closes)]
438 }
439 Geometry::Surface { surface } => {
440 let (nu, nv) = (surface.num_u, surface.num_v);
441 let at = |i: usize, j: usize| surface.control_points[i * nv + j];
442 let row = |i: usize| (0..nv).map(|j| at(i, j)).collect::<Vec<_>>();
443 let column = |j: usize| (0..nu).map(|i| at(i, j)).collect::<Vec<_>>();
444 vec![
445 foot(surface.domain_u(), ends(&row(0), &row(nu - 1))),
446 foot(surface.domain_v(), ends(&column(0), &column(nv - 1))),
447 ]
448 }
449 _ => Vec::new(),
450 }
451 }
452}
453
454#[derive(Clone, Copy, Debug, PartialEq)]
458pub struct Foot {
459 pub lo: Scalar,
460 pub hi: Scalar,
461 pub closes: bool,
462}
463
464impl Foot {
465 fn round(&self, x: D) -> D {
469 if !self.closes {
470 return x;
471 }
472 let (lo, period) = (self.lo.to_f64(), self.hi.sub(self.lo).to_f64());
473 let turns = ((x.v.to_f64() - lo) / period).floor();
474 match turns == 0.0 {
475 true => x,
476 false => x.sub(D::cst(self.hi.sub(self.lo).mul(Scalar::from_f64(turns)))),
477 }
478 }
479}
480
481fn first_order(value: &Vector3, steps: &[(&Vector3, &D)]) -> Vector3<D> {
484 Vector3::from_array(std::array::from_fn(|i| {
485 (steps.iter()).fold(D::cst(value[i]), |sum, (slope, by)| {
486 sum.add(by.mul(D::cst(slope[i])))
487 })
488 }))
489}
490
491pub fn contact_residuals(
496 kind: &Kind,
497 a: &Geometry,
498 pa: Option<&Pose<D>>,
499 b: &Geometry,
500 pb: Option<&Pose<D>>,
501 feet: &[D],
502) -> GeopResult<Vec<D>> {
503 let ctx = with_context!("a {} constraint", kind.label().to_lowercase());
504 let foot = a.foot(feet, b.radius()).with_context(ctx)?;
505 let on = Placing::new(pa).moved(&foot);
506 Ok(b.follower(pb)
507 .with_context(ctx)?
508 .sub(&on)
509 .to_array()
510 .to_vec())
511}
512
513pub fn seed_feet(
518 a: &Geometry,
519 pa: Option<&Pose>,
520 b: &Geometry,
521 pb: Option<&Pose>,
522) -> GeopResult<Vec<Scalar>> {
523 let at = match b {
524 Geometry::Point { at } | Geometry::Rim { center: at, .. } => *at,
525 _ => return Err(GeopError::new(format!("{} follows nothing", b.kind()))),
526 };
527 let world = pb.map_or(at, |pose| pose.apply(&at));
528 let local = pa.map_or(world, |pose| pose.inverse().apply(&world));
529 match a {
530 Geometry::Curve { curve } => Ok(vec![curve.closest_point(&local)?.0]),
531 Geometry::Surface { surface } => {
532 let (u, v, _) = surface.closest_point(&local)?;
533 Ok(vec![u, v])
534 }
535 _ => Err(GeopError::new(format!("{} has no foot point", a.kind()))),
536 }
537}
538
539pub fn residuals(kind: &Kind, a: &World, b: &World, scale: D) -> GeopResult<Vec<D>> {
542 use World::*;
543 let l = scale;
544 let mut out = Vec::new();
545 let parallel = |d: &Direction, e: &Direction, out: &mut Vec<D>| {
548 out.extend(self::parallel(&d.across, &e.along, l))
549 };
550 let off_line = |p: &Vector3<D>, q: &Vector3<D>, d: &Direction, out: &mut Vec<D>| {
553 out.extend(d.across.map(|x| x.prod_dot(&p.sub(q))))
554 };
555 let distance_off_line =
556 |p: &Vector3<D>, q: &Vector3<D>, d: &Direction| p.sub(q).prod_cross(&d.along).norm();
557 let along = |p: &Vector3<D>, q: &Vector3<D>, n: &Direction| n.along.prod_dot(&p.sub(q));
558 let dot = |d: &Direction, e: &Direction| d.along.prod_dot(&e.along).mul(l);
559 let directions = || -> GeopResult<(&Direction, &Direction)> {
560 match (a.direction(), b.direction()) {
561 (Some(d), Some(e)) => Ok((d, e)),
562 _ => Err(GeopError::new(format!(
563 "a {} constraint needs two directions",
564 kind.label().to_lowercase()
565 ))),
566 }
567 };
568 match kind {
569 Kind::Coincident => match (a, b) {
570 (Point(p), Point(q)) => out.extend(p.sub(q).to_array()),
571 (Point(p), Line(q, d)) | (Line(q, d), Point(p)) => off_line(p, q, d, &mut out),
572 (Point(p), Plane(q, n)) | (Plane(q, n), Point(p)) => out.push(along(p, q, n)),
573 (Line(p, d), Line(q, e)) => {
574 parallel(d, e, &mut out);
575 off_line(q, p, d, &mut out);
576 }
577 (Line(p, d), Plane(q, n)) | (Plane(q, n), Line(p, d)) => {
578 out.push(dot(d, n));
579 out.push(along(p, q, n));
580 }
581 (Plane(p, n), Plane(q, m)) => {
582 parallel(n, m, &mut out);
583 out.push(along(q, p, n));
584 }
585 },
586 Kind::Concentric => match (a, b) {
588 (Line(..), Line(..)) => return residuals(&Kind::Coincident, a, b, scale),
589 _ => return Err(GeopError::new("a concentric constraint needs two lines")),
590 },
591 Kind::Parallel | Kind::Perpendicular => {
592 let (d, e) = directions()?;
593 let mixed = matches!((a, b), (Line(..), Plane(..)) | (Plane(..), Line(..)));
596 match matches!(kind, Kind::Parallel) != mixed {
597 true => parallel(d, e, &mut out),
598 false => out.push(dot(d, e)),
599 }
600 }
601 Kind::Distance { value } => {
602 let value = D::cst(*value);
603 match (a, b) {
604 (Line(p, d), Line(q, e)) => {
605 parallel(d, e, &mut out);
606 out.push(distance_off_line(q, p, d).sub(value));
607 }
608 (Line(p, d), Plane(q, n)) | (Plane(q, n), Line(p, d)) => {
609 out.push(dot(d, n));
610 out.push(along(p, q, n).abs().sub(value));
611 }
612 (Plane(p, n), Plane(q, m)) => {
613 parallel(n, m, &mut out);
614 out.push(along(q, p, n).abs().sub(value));
615 }
616 (Point(p), Point(q)) => out.push(p.sub(q).norm().sub(value)),
617 (Point(p), Line(q, d)) | (Line(q, d), Point(p)) => {
618 out.push(distance_off_line(p, q, d).sub(value))
619 }
620 (Point(p), Plane(q, n)) | (Plane(q, n), Point(p)) => {
621 out.push(along(p, q, n).abs().sub(value))
622 }
623 }
624 }
625 Kind::Angle { value } => {
637 let (d, e) = directions()?;
638 let radians = D::cst(*value).degrees_to_radians()?;
639 let sin = radians.sin();
640 let cos = d.along.prod_dot(&e.along).sub(radians.cos());
641 if sin.could_be_equal(D::ZERO) {
642 parallel(d, e, &mut out);
643 out.push(cos.mul(l));
644 } else {
645 out.push(cos.mul(D::ONE.div(sin)?).mul(l));
646 }
647 }
648 Kind::Tangent | Kind::Path => {
649 return Err(GeopError::new(format!(
650 "a {} constraint holds where its foot point is: see `contact_residuals`",
651 kind.label().to_lowercase()
652 )));
653 }
654 }
655 Ok(out)
656}