1use geop_core_geometry::nurb_curve::NurbCurve3D;
55use geop_core_math::{
56 geop_error::{GeopError, GeopResult, WithContext},
57 primitives::DatumKind,
58 scalars::Scalar,
59 vector::Vector3,
60 with_context,
61};
62use geop_core_program::reader::Reader;
63use geop_ops_model::Chain;
64use geop_ops_model::mate_entity::MateEntity;
65use geop_ops_model::parameters::located;
66use geop_ops_model::selection::{Selected, Shape};
67
68#[derive(Clone, Debug)]
70pub enum Heading {
71 Free,
73 Axis(Vector3),
75 Exit(Vector3),
77}
78
79#[derive(Clone, Debug)]
81pub struct Waypoint {
82 pub point: Vector3,
83 pub heading: Heading,
84 pub label: String,
87}
88
89impl Heading {
90 fn turned(self, turn: impl Fn(&Vector3) -> Vector3) -> Self {
92 match self {
93 Heading::Free => Heading::Free,
94 Heading::Axis(d) => Heading::Axis(turn(&d)),
95 Heading::Exit(d) => Heading::Exit(turn(&d)),
96 }
97 }
98}
99
100impl Waypoint {
101 pub fn of(reader: &Reader, entity: &Selected, index: usize) -> GeopResult<Self> {
106 let frame_on = MateEntity::on(entity.clone());
107 let label = format!("point {} ({})", index + 1, frame_on.label());
108 let ctx = with_context!("resolving the route's {label}");
109 let unsupported = |what: &str| {
110 GeopError::new(format!(
111 "route: {label} is {what}: a route runs through points, coordinate systems (along their z axis) and circular edges (through their centre, along their axis)"
112 ))
113 };
114 let (inside, pose) = match entity.instance() {
115 "" => (reader.clone(), None),
116 part => {
117 let inside = reader.placed_part(part).with_context(ctx)?;
118 (inside, Some(located(reader, part).with_context(ctx)?))
119 }
120 };
121 let local = entity.local();
122 let (point, heading) = if let Selected::Datum(datum) = &local {
123 let datum = inside.retrieve_element(datum).with_context(ctx)?;
124 let heading = match datum.kind {
125 DatumKind::Point => Heading::Free,
126 DatumKind::Frame => Heading::Exit(*datum.frame.w()),
127 DatumKind::Axis => return Err(unsupported("an axis")),
128 DatumKind::Plane => return Err(unsupported("a plane")),
129 };
130 (*datum.frame.origin(), heading)
131 } else {
132 let shape = local.shape(&inside).with_context(ctx)?;
133 match (&local, shape) {
134 (
135 Selected::Vertex(_) | Selected::SketchPoint(_),
136 Shape {
137 point: Some(point), ..
138 },
139 ) => (point, Heading::Free),
140 (_, Shape { arc: Some(arc), .. }) => {
141 (arc.circle.center, Heading::Axis(arc.circle.normal))
142 }
143 _ => {
145 let frame = frame_on.frame(reader);
146 let frame = frame.with_context(ctx)?;
147 (*frame.origin(), Heading::Exit(*frame.w()))
148 }
149 }
150 };
151 let (point, heading) = match pose {
152 Some(pose) => {
153 let motion = pose.motion();
154 let heading = heading.turned(|d| motion.rotate(d));
155 (motion.apply(&point), heading)
156 }
157 None => (point, heading),
158 };
159 Ok(Waypoint {
160 point,
161 heading,
162 label,
163 })
164 }
165}
166
167#[derive(Clone, Debug)]
173pub struct RoutePath {
174 pub chain: Chain<NurbCurve3D>,
175 pub places: Vec<String>,
178}
179
180#[derive(Clone, Debug)]
182pub struct Measure {
183 pub length: Scalar,
185 pub bends: Vec<Bend>,
187}
188
189#[derive(Clone, Debug)]
191pub struct Bend {
192 pub curve: usize,
193 pub radius: Scalar,
194}
195
196impl Measure {
197 pub fn tightest(&self) -> Option<&Bend> {
199 self.bends
200 .iter()
201 .min_by(|a, b| a.radius.to_f64().total_cmp(&b.radius.to_f64()))
202 }
203
204 pub fn too_tight(&self, min_radius: f64) -> Vec<&Bend> {
207 let min = Scalar::from_f64(min_radius);
208 self.bends
209 .iter()
210 .filter(|b| b.radius.definitely_less(min))
211 .collect()
212 }
213}
214
215impl RoutePath {
216 pub fn through(waypoints: &[Waypoint]) -> GeopResult<Self> {
223 let n = waypoints.len();
224 if n < 2 {
225 return Err(GeopError::new(format!(
226 "route: pick the points the route runs through, two at least — {n} picked"
227 )));
228 }
229 let tangents = tangents(waypoints)?;
230 let label = |i: usize| &waypoints[i].label;
231 let mut chain = Chain {
232 name: "route".to_string(),
233 curves: Vec::new(),
234 curve_names: Vec::new(),
235 joint_names: Vec::new(),
236 };
237 let mut places = Vec::new();
238 for i in 0..n - 1 {
239 let ctx = with_context!("routing from {} to {}", label(i), label(i + 1));
240 let (p0, p1) = (&waypoints[i].point, &waypoints[i + 1].point);
241 let place = format!("between {} and {}", label(i), label(i + 1));
242 let [first, second] =
243 biarc(&place, p0, &tangents[i], p1, &tangents[i + 1]).with_context(ctx)?;
244 let mut push = |curve: NurbCurve3D, name: String, joint: String| {
245 chain.curves.push(curve);
246 chain.curve_names.push(name);
247 chain.joint_names.push(joint);
248 places.push(place.clone());
249 };
250 if first.straight && second.straight {
251 push(
252 NurbCurve3D::line(*p0, *p1)?,
253 format!("a{i}"),
254 format!("w{i}"),
255 );
256 } else {
257 push(first.curve, format!("a{i}"), format!("w{i}"));
258 push(second.curve, format!("b{i}"), format!("m{i}"));
259 }
260 }
261 chain.joint_names.push(format!("w{}", n - 1));
262 Ok(RoutePath { chain, places })
263 }
264
265 pub fn measure(&self) -> GeopResult<Measure> {
269 let mut length = Scalar::ZERO;
270 let mut bends = Vec::new();
271 for (k, curve) in self.chain.curves.iter().enumerate() {
272 let ctx = with_context!("measuring the route {}", self.places[k]);
273 if let Some(arc) = curve.as_arc().with_context(ctx)? {
276 length = length.add(arc.length().with_context(ctx)?);
277 bends.push(Bend {
278 curve: k,
279 radius: arc.circle.radius,
280 });
281 } else if curve.as_line().with_context(ctx)?.is_some() {
282 let (t0, t1) = curve.domain();
283 let chord = curve.evaluate(t1)?.sub(&curve.evaluate(t0)?);
284 length = length.add(chord.norm());
285 } else {
286 return Err(GeopError::new(format!(
287 "route: the route {} is neither straight nor a circular arc, and only those are measured",
288 self.places[k]
289 )));
290 }
291 }
292 Ok(Measure { length, bends })
293 }
294}
295
296fn tangents(waypoints: &[Waypoint]) -> GeopResult<Vec<Vector3>> {
299 let n = waypoints.len();
300 let label = |i: usize| &waypoints[i].label;
301 let chords = (0..n - 1)
302 .map(|i| {
303 let d = waypoints[i + 1].point.sub(&waypoints[i].point);
304 if d.norm_sq().could_be_equal(Scalar::ZERO) {
305 return Err(GeopError::new(format!(
306 "route: {} and {} could be one point: the route runs between points apart",
307 label(i),
308 label(i + 1)
309 )));
310 }
311 d.normalize()
312 })
313 .collect::<GeopResult<Vec<_>>>()?;
314 let going = |i: usize| match (i.checked_sub(1).map(|j| chords[j]), chords.get(i)) {
317 (Some(a), Some(b)) => a.add(b),
318 (Some(a), None) => a,
319 (None, Some(b)) => *b,
320 (None, None) => unreachable!("a route has two waypoints at least"),
321 };
322 let mut tangents: Vec<Option<Vector3>> = Vec::with_capacity(n);
323 for (i, waypoint) in waypoints.iter().enumerate() {
324 tangents.push(match &waypoint.heading {
325 Heading::Exit(d) if i + 1 == n => Some(d.neg().normalize()?),
326 Heading::Exit(d) => Some(d.normalize()?),
327 Heading::Axis(axis) => {
328 let along = axis.prod_dot(&going(i));
329 if along.definitely_greater(Scalar::ZERO) {
330 Some(axis.normalize()?)
331 } else if along.definitely_less(Scalar::ZERO) {
332 Some(axis.neg().normalize()?)
333 } else {
334 return Err(GeopError::new(format!(
335 "route: the axis of {} could lie square to the route, which then has no way to pass through it: move the points before or after it",
336 label(i)
337 )));
338 }
339 }
340 Heading::Free if i == 0 || i + 1 == n => None,
341 Heading::Free => {
342 let bisector = going(i);
343 if bisector.norm_sq().could_be_equal(Scalar::ZERO) {
344 return Err(GeopError::new(format!(
345 "route: the route could turn right back at {}",
346 label(i)
347 )));
348 }
349 Some(bisector.sharpen().normalize()?)
353 }
354 });
355 }
356 let mirrored = |t: &Vector3, c: &Vector3| {
360 c.prod_scalar(Scalar::TWO.mul(c.prod_dot(t)))
361 .sub(t)
362 .sharpen()
363 .normalize()
364 };
365 if n == 2 && tangents.iter().all(Option::is_none) {
366 return Ok(vec![chords[0]; 2]);
367 }
368 if tangents[0].is_none() {
369 let next = tangents[1].expect("set: a free end with a free neighbour is a route of two");
370 tangents[0] = Some(mirrored(&next, &chords[0])?);
371 }
372 if tangents[n - 1].is_none() {
373 let before = tangents[n - 2].expect("set before");
374 tangents[n - 1] = Some(mirrored(&before, &chords[n - 2])?);
375 }
376 Ok(tangents.into_iter().map(|t| t.expect("all set")).collect())
377}
378
379struct Piece {
381 curve: NurbCurve3D,
382 straight: bool,
383}
384
385fn biarc(
388 place: &str,
389 p0: &Vector3,
390 t0: &Vector3,
391 p1: &Vector3,
392 t1: &Vector3,
393) -> GeopResult<[Piece; 2]> {
394 let v = p1.sub(p0);
395 let a = Scalar::TWO.mul(t0.prod_dot(t1).sub(Scalar::ONE));
396 let b = Scalar::TWO.mul(v.prod_dot(&t0.add(t1))).neg();
397 let c = v.norm_sq();
398 let root = b
399 .mul(b)
400 .sub(Scalar::TWO.mul(Scalar::TWO).mul(a).mul(c))
401 .sqrt()?;
402 let denominator = root.sub(b);
405 let numbers = || format!("heading {t0:?} at {p0:?} and {t1:?} at {p1:?}");
406 if !denominator.definitely_greater(Scalar::ZERO) {
407 return Err(GeopError::new(format!(
408 "route: {place}, the route would have to loop round to arrive: add a waypoint between them"
409 ))
410 .with_context(numbers()));
411 }
412 let d = Scalar::TWO.mul(c).div(denominator)?;
413 let q1 = p0.add(&t0.prod_scalar(d));
414 let q2 = p1.sub(&t1.prod_scalar(d));
415 let joint = q1.add(&q2).prod_scalar(Scalar::ONE.div(Scalar::TWO)?);
416 let turns_back = || {
417 GeopError::new(format!(
418 "route: {place}, the route would have to turn right back on itself: add a waypoint between them"
419 ))
420 .with_context(numbers())
421 };
422 match [piece(p0, &q1, &joint)?, piece(&joint, &q2, p1)?] {
423 [Some(first), Some(second)] => Ok([first, second]),
424 _ => Err(turns_back()),
425 }
426}
427
428fn piece(a: &Vector3, q: &Vector3, b: &Vector3) -> GeopResult<Option<Piece>> {
432 let (first, second) = (q.sub(a), b.sub(q));
433 let dot = first.prod_dot(&second);
434 if first
435 .prod_cross(&second)
436 .norm_sq()
437 .could_be_equal(Scalar::ZERO)
438 {
439 if !dot.definitely_greater(Scalar::ZERO) {
440 return Ok(None);
441 }
442 return Ok(Some(Piece {
443 curve: NurbCurve3D::line(*a, *b)?,
444 straight: true,
445 }));
446 }
447 let cos = dot.div(first.norm().mul(second.norm()))?;
449 let weight = Scalar::ONE.add(cos).div(Scalar::TWO)?.sqrt()?;
450 Ok(Some(Piece {
451 curve: NurbCurve3D::arc(*a, *q, *b, weight)?,
452 straight: false,
453 }))
454}