1use std::collections::BTreeMap;
27
28use geop_core_geometry::nurb_curve::NurbCurve3D;
29use geop_core_math::solvers::system::{self, Phase};
30use geop_core_math::{
31 geop_error::{GeopError, GeopResult},
32 scalars::{Number, Scalar, as_f64},
33 vector::Vector3,
34};
35use serde::{Deserialize, Serialize};
36
37use super::{
38 Constraint3d, CurveKind3d, Enclosure3d, End, Point3d, Sketch3d,
39 geometry::{Arc3, Helix3, across},
40};
41use crate::{
42 ConstraintId, CurveId, PointId,
43 variables::{PointLayout, Residuals, SketchPoint, Standing, Variables},
44};
45
46const MAX_LOCAL_VARS: usize = 16;
49
50type Dual = geop_core_math::dual::Dual<MAX_LOCAL_VARS>;
51
52struct Layout {
56 points: PointLayout<3>,
57 param_var: BTreeMap<ConstraintId, usize>,
60}
61
62impl SketchPoint<3> for Point3d {
63 fn fixed(&self) -> bool {
64 self.fixed
65 }
66
67 fn coordinates(&self) -> [Scalar; 3] {
68 self.at.to_array()
69 }
70
71 fn set_coordinates(&mut self, at: [Scalar; 3]) {
72 self.at = Vector3::from_array(at);
73 }
74}
75
76impl Layout {
77 fn new(sketch: &Sketch3d) -> Self {
78 let mut points = PointLayout::new(&sketch.points, &sketch.point_classes());
79 let mut param_var = BTreeMap::new();
80 for (&id, c) in &sketch.constraints {
81 if let Constraint3d::OnCurve { curve, .. } = c
82 && matches!(
83 sketch.curves[curve].kind,
84 CurveKind3d::Line { .. } | CurveKind3d::Reference
85 )
86 {
87 param_var.insert(id, points.push(true));
88 }
89 }
90 Layout { points, param_var }
91 }
92}
93
94struct Prepared<'a> {
96 id: ConstraintId,
97 constraint: &'a Constraint3d,
98 vars: Vec<usize>,
99 across: Option<[Vector3; 2]>,
102 reference: Option<&'a NurbCurve3D>,
104}
105
106struct Problem<'a> {
108 sketch: &'a Sketch3d,
109 layout: Layout,
110 constraints: Vec<Prepared<'a>>,
111 start: Vec<Scalar>,
114 scale: Scalar,
117}
118
119fn point_of<T: Number>(layout: &Layout, x: &[T], p: PointId) -> Vector3<T> {
121 Vector3::from_array(layout.points.point(x, p))
122}
123
124fn arc_of<T: Number>(sketch: &Sketch3d, layout: &Layout, x: &[T], c: CurveId) -> Option<Arc3<T>> {
127 match sketch.curves[&c].kind {
128 CurveKind3d::Arc {
129 start,
130 through,
131 end,
132 } => Some(Arc3 {
133 s: point_of(layout, x, start),
134 m: point_of(layout, x, through),
135 e: point_of(layout, x, end),
136 }),
137 _ => None,
138 }
139}
140
141fn line_of<T: Number>(
143 sketch: &Sketch3d,
144 layout: &Layout,
145 x: &[T],
146 c: CurveId,
147) -> (Vector3<T>, Vector3<T>) {
148 match sketch.curves[&c].kind {
149 CurveKind3d::Line { start, end } => (point_of(layout, x, start), point_of(layout, x, end)),
150 ref k => unreachable!("validated to be a line: {k:?}"),
151 }
152}
153
154fn tangent_of<T: Number>(
157 sketch: &Sketch3d,
158 layout: &Layout,
159 x: &[T],
160 c: CurveId,
161 end: End,
162) -> GeopResult<Vector3<T>> {
163 match arc_of(sketch, layout, x, c) {
164 Some(arc) => arc.tangent(end == End::End),
165 None => {
166 let (s, e) = line_of(sketch, layout, x, c);
167 e.sub(&s).normalize()
168 }
169 }
170}
171
172impl<'a> Problem<'a> {
173 fn new(sketch: &'a Sketch3d) -> GeopResult<Self> {
174 sketch.validate()?;
175 let layout = Layout::new(sketch);
176 let mut start = vec![Scalar::ZERO; layout.points.len()];
177 layout.points.read(&sketch.points, &mut start);
178
179 let span = |k: usize| {
180 let values = || sketch.points.values().map(move |p| p.at[k]);
181 let hi = values().reduce(Scalar::max).unwrap_or(Scalar::ZERO);
182 let lo = values().reduce(Scalar::min).unwrap_or(Scalar::ZERO);
183 hi.sub(lo)
184 };
185 let diagonal = (0..3)
186 .map(|k| span(k).mul(span(k)))
187 .fold(Scalar::ZERO, Scalar::add)
188 .sqrt()?;
189 let scale = if diagonal.is_finite() && diagonal.definitely_greater(Scalar::ONE) {
190 diagonal
191 } else {
192 Scalar::ONE
193 };
194
195 let mut constraints = Vec::new();
196 for (&id, c) in &sketch.constraints {
197 use Constraint3d::*;
198 let mut vars = Vec::new();
199 let mut across_ = None;
200 let mut reference = None;
201 let curve_vars = |c: CurveId, vars: &mut Vec<usize>| {
202 for p in sketch.curves[&c].points() {
203 layout.points.point_vars(p, vars);
204 }
205 };
206 match c {
207 Coincident { a, b } if sketch.points[a].fixed && sketch.points[b].fixed => {}
208 Coincident { .. } => continue,
210 Coordinate { point, .. } => layout.points.point_vars(*point, &mut vars),
211 Distance { a, b, .. } => {
212 layout.points.point_vars(*a, &mut vars);
213 layout.points.point_vars(*b, &mut vars);
214 }
215 Length { line: c, .. } | Radius { arc: c, .. } | HelixEnd { helix: c } => {
216 curve_vars(*c, &mut vars);
217 }
218 Parallel { a, b } => {
219 curve_vars(*a, &mut vars);
220 curve_vars(*b, &mut vars);
221 let (s, e) = line_of(sketch, &layout, &start, *a);
222 across_ = Some(across(&e.sub(&s))?);
223 }
224 ParallelTo { line, direction } => {
225 curve_vars(*line, &mut vars);
226 across_ = Some(across(direction)?);
227 }
228 TangentTo { curve, .. }
230 if matches!(sketch.curves[curve].kind, CurveKind3d::Spline { .. }) =>
231 {
232 continue;
233 }
234 TangentTo {
235 curve, direction, ..
236 } => {
237 curve_vars(*curve, &mut vars);
238 across_ = Some(across(direction)?);
239 }
240 OnCurve { point, curve } => {
241 layout.points.point_vars(*point, &mut vars);
242 curve_vars(*curve, &mut vars);
243 if let Some(&t) = layout.param_var.get(&id) {
244 vars.push(t);
245 let p = point_of(&layout, &start, *point);
246 let seed = match &sketch.curves[curve].kind {
247 CurveKind3d::Line { .. } => {
248 let (s, e) = line_of(sketch, &layout, &start, *curve);
249 let d = e.sub(&s);
250 p.sub(&s).prod_dot(&d).div(d.norm_sq())?
251 }
252 _ => {
253 let curve = &sketch.references[curve];
254 reference = Some(curve);
255 curve.closest_point(&p)?.0
260 }
261 };
262 start[t] = Scalar::from_f64(seed.to_f64());
264 }
265 }
266 Tangent { a, b } => {
267 if sketch.adopts(*a, *b)?.is_some() {
268 continue;
269 }
270 curve_vars(*a, &mut vars);
271 curve_vars(*b, &mut vars);
272 let (ea, _) = sketch.shared_end(*a, *b)?.expect("validated");
273 across_ = Some(across(&tangent_of(sketch, &layout, &start, *a, ea)?)?);
274 }
275 }
276 vars.sort_unstable();
277 vars.dedup();
278 if vars.len() > MAX_LOCAL_VARS {
279 return Err(GeopError::new(format!(
280 "constraint {id} depends on {} variables, more than the supported {MAX_LOCAL_VARS}",
281 vars.len()
282 )));
283 }
284 constraints.push(Prepared {
285 id,
286 constraint: c,
287 vars,
288 across: across_,
289 reference,
290 });
291 }
292 Ok(Problem {
293 sketch,
294 layout,
295 constraints,
296 start,
297 scale,
298 })
299 }
300
301 fn residuals(&self, p: &Prepared<'_>, x: &[Dual], out: &mut Vec<Dual>) -> GeopResult<()> {
304 use Constraint3d::*;
305 let c = Dual::cst;
306 let scale = c(self.scale);
307 let (sketch, layout) = (self.sketch, &self.layout);
308 let point = |q: PointId| point_of(layout, x, q);
309 let parallel = |a: &Vector3<Dual>, b: &Vector3<Dual>, out: &mut Vec<Dual>| {
311 let cross = a.prod_cross(b);
312 for u in p.across.as_ref().expect("prepared") {
313 out.push(cross.prod_dot(&u.map(c)).mul(scale));
314 }
315 };
316 match *p.constraint {
317 Coincident { a, b } => {
318 let d = sketch.points[&b].at.sub(&sketch.points[&a].at);
319 out.extend(d.to_array().map(c));
320 }
321 Coordinate {
322 point: q,
323 axis,
324 value,
325 } => {
326 out.push(point(q)[axis.index()].sub(c(value)));
327 }
328 Distance { a, b, value } => out.push(point(b).sub(&point(a)).try_norm()?.sub(c(value))),
329 Length { line, value } => {
330 let (s, e) = line_of(sketch, layout, x, line);
331 out.push(e.sub(&s).try_norm()?.sub(c(value)));
332 }
333 Radius { arc, value } => {
334 let arc = arc_of(sketch, layout, x, arc).expect("validated");
335 out.push(arc.radius()?.sub(c(value)));
336 }
337 HelixEnd { helix } => {
338 let CurveKind3d::Helix {
339 start,
340 base,
341 top,
342 end,
343 turns,
344 left,
345 } = sketch.curves[&helix].kind
346 else {
347 unreachable!("validated to be a helix");
348 };
349 let helix = Helix3 {
350 start: point(start),
351 base: point(base),
352 top: point(top),
353 turns,
354 left,
355 };
356 out.extend(point(end).sub(&helix.end()?).to_array());
357 }
358 Parallel { a, b } => {
359 let (sa, ea) = line_of(sketch, layout, x, a);
360 let (sb, eb) = line_of(sketch, layout, x, b);
361 parallel(&ea.sub(&sa).normalize()?, &eb.sub(&sb).normalize()?, out);
362 }
363 ParallelTo { line, direction } => {
364 let (s, e) = line_of(sketch, layout, x, line);
365 parallel(&e.sub(&s).normalize()?, &direction.map(c).normalize()?, out);
366 }
367 TangentTo {
368 curve,
369 end,
370 direction,
371 } => {
372 let t = tangent_of(sketch, layout, x, curve, end)?;
373 parallel(&t, &direction.map(c).normalize()?, out);
374 }
375 OnCurve { point: q, curve } => {
376 let q = point(q);
377 let on = match (&sketch.curves[&curve].kind, layout.param_var.get(&p.id)) {
378 (CurveKind3d::Arc { .. }, _) => {
379 let arc = arc_of(sketch, layout, x, curve).expect("an arc");
380 let center = arc.center()?;
381 out.push(q.sub(&arc.m).prod_dot(&arc.normal()?));
382 out.push(q.sub(¢er).try_norm()?.sub(arc.radius()?));
383 return Ok(());
384 }
385 (CurveKind3d::Line { .. }, Some(&t)) => {
386 let (s, e) = line_of(sketch, layout, x, curve);
387 s.add(&e.sub(&s).prod_scalar(x[t]))
388 }
389 (_, Some(&t)) => {
390 let curve = p.reference.expect("prepared");
394 let (at, tangent) = (curve.evaluate(x[t].v)?, curve.tangent(x[t].v)?);
395 Vector3::from_array([0, 1, 2].map(|i| x[t].chain(at[i], tangent[i])))
396 }
397 (k, None) => unreachable!("a point on {k:?} has a parameter"),
398 };
399 out.extend(q.sub(&on).to_array());
400 }
401 Tangent { a, b } => {
402 let (ea, eb) = sketch.shared_end(a, b)?.expect("validated");
403 let ta = tangent_of(sketch, layout, x, a, ea)?;
404 let tb = tangent_of(sketch, layout, x, b, eb)?;
405 parallel(&ta, &tb, out);
406 }
407 }
408 Ok(())
409 }
410
411 fn report(
414 &self,
415 params: Vec<Scalar>,
416 phases: Vec<Phase>,
417 solved: Option<system::Enclosure<Scalar>>,
418 ) -> GeopResult<Solve3dReport> {
419 let variables = Spatial::new(self, params, Vec::new());
420 let Standing {
421 held,
422 dof,
423 free: free_vars,
424 unproven,
425 } = variables.standing(&variables.start(), solved)?;
426 let failed_constraints = (held.failed.iter())
427 .map(|&i| self.constraints[i].id)
428 .collect::<Vec<_>>();
429 let points = &self.layout.points;
430 let free_points = (points.point_var.keys())
431 .map(|&p| (p, points.point(&free_vars, p).contains(&true)))
432 .collect();
433 Ok(Solve3dReport {
434 converged: failed_constraints.is_empty(),
435 max_residual: held.max_residual,
436 iterations: phases.iter().map(|p| p.iterations).sum(),
437 phases,
438 dof,
439 free_points,
440 failed_constraints,
441 unproven,
442 })
443 }
444}
445
446impl Residuals<MAX_LOCAL_VARS> for Problem<'_> {
447 fn offsets(&self) -> Vec<Option<usize>> {
448 self.layout.points.offsets()
449 }
450
451 fn constraints(&self) -> usize {
452 self.constraints.len()
453 }
454
455 fn reads(&self, c: usize) -> &[usize] {
456 &self.constraints[c].vars
457 }
458
459 fn residuals(&self, c: usize, values: &[Dual], out: &mut Vec<Dual>) -> GeopResult<()> {
460 let prepared = &self.constraints[c];
461 let mut x = vec![Dual::cst(Scalar::ZERO); self.layout.points.len()];
463 for (&i, &v) in prepared.vars.iter().zip(values) {
464 x[i] = v;
465 }
466 Problem::residuals(self, prepared, &x, out)
467 }
468
469 fn name(&self, c: usize) -> String {
470 let c = &self.constraints[c];
471 format!("{} {:?}", c.id, c.constraint)
472 }
473
474 fn scale(&self) -> Scalar {
475 self.scale
476 }
477}
478
479type Spatial<'p, 'a> = Variables<'p, Problem<'a>, MAX_LOCAL_VARS>;
481
482#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
484pub struct Solve3dReport {
485 pub converged: bool,
489 #[serde(with = "as_f64")]
491 pub max_residual: Scalar,
492 pub iterations: usize,
493 pub phases: Vec<Phase>,
494 pub dof: usize,
496 pub free_points: BTreeMap<PointId, bool>,
498 pub failed_constraints: Vec<ConstraintId>,
500 #[serde(default)]
504 pub unproven: Vec<String>,
505}
506
507impl Sketch3d {
508 pub fn solve(&mut self) -> GeopResult<Solve3dReport> {
512 self.solve_with_drag(&[])
513 }
514
515 pub fn solve_with_drag(&mut self, drags: &[(PointId, Vector3)]) -> GeopResult<Solve3dReport> {
518 self.seat_helix_ends()?;
519 let mut problem = Problem::new(self)?;
520 let helices = (problem.constraints.iter())
524 .any(|c| matches!(c.constraint, Constraint3d::HelixEnd { .. }));
525 (problem.constraints).retain(|c| !matches!(c.constraint, Constraint3d::HelixEnd { .. }));
526 let (_, solved, x) = Spatial::dragged(
527 &problem,
528 problem.start.clone(),
529 &problem.layout.points.point_var,
530 drags,
531 )?;
532 let report = problem.report(x.clone(), solved.phases.clone(), Some(solved.enclosure))?;
533 let x: Vec<Scalar> = x.into_iter().map(Scalar::sharpen).collect();
536 problem.layout.points.write(&mut self.points, &x);
537 if !helices {
538 return Ok(report);
539 }
540 self.seat_helix_ends()?;
541 let whole = Problem::new(self)?;
542 whole.report(whole.start.clone(), solved.phases, None)
543 }
544
545 fn seat_helix_ends(&mut self) -> GeopResult<()> {
549 let helices: Vec<_> = (self.curves.values())
550 .filter_map(|curve| match curve.kind {
551 CurveKind3d::Helix {
552 start,
553 base,
554 top,
555 end,
556 turns,
557 left,
558 } => Some((start, base, top, end, turns, left)),
559 _ => None,
560 })
561 .collect();
562 for (start, base, top, end, turns, left) in helices {
563 let at = (Helix3 {
564 start: self.point(start)?.at,
565 base: self.point(base)?.at,
566 top: self.point(top)?.at,
567 turns,
568 left,
569 })
570 .end()?;
571 if let Some(end) = self.points.get_mut(&end)
572 && !end.fixed
573 {
574 end.at = at.sharpen();
575 }
576 }
577 Ok(())
578 }
579
580 pub fn check(&self) -> GeopResult<Solve3dReport> {
583 let problem = Problem::new(self)?;
584 problem.report(problem.start.clone(), Vec::new(), None)
585 }
586
587 pub fn enclose(&self) -> GeopResult<Enclosure3d> {
593 let ctx = |e: GeopError| e.with_context("enclosing the 3-D sketch's solution");
594 let problem = Problem::new(self).map_err(ctx)?;
595 let variables = Spatial::new(&problem, problem.start.clone(), Vec::new());
596 let start = variables.start();
597 if !variables.system().held(&start)?.converged {
598 return Ok(Enclosure3d::as_drawn(self));
599 }
600 let x = variables.enclosed(&start).map_err(ctx)?;
602 let points = &problem.layout.points;
603 Ok(Enclosure3d {
604 points: (points.point_var.keys())
605 .map(|&p| (p, Vector3::from_array(points.point(&x, p))))
606 .collect(),
607 })
608 }
609}