1use crate::{
27 bfgs::{BfgsOptions, minimize},
28 dual::{Dual, MAX_LOCAL_VARS},
29 geometry::{Arc, V, line_distance},
30 sketch::{Constraint, ConstraintId, CurveId, CurveKind, PointId, Sketch},
31};
32use geop_core_math::{
33 geop_error::{GeopError, GeopResult},
34 scalars::{Scalar, scal_in_f64::ScalInF64},
35};
36
37type S = ScalInF64;
40
41use serde::Serialize;
42use std::collections::BTreeMap;
43
44struct Layout {
46 point_var: BTreeMap<PointId, usize>,
48 curve_var: BTreeMap<CurveId, usize>,
50 n: usize,
51}
52
53impl Layout {
54 fn new(sketch: &Sketch) -> Self {
55 let class = sketch.point_classes();
56 let mut n = 0;
57 let mut class_var = BTreeMap::new();
58 for rep in class.values() {
59 class_var.entry(*rep).or_insert_with(|| {
60 n += 2;
61 n - 2
62 });
63 }
64 let point_var = class.iter().map(|(&p, rep)| (p, class_var[rep])).collect();
65 let curve_var = sketch
66 .curves
67 .iter()
68 .filter(|(_, c)| matches!(c.kind, CurveKind::Arc { .. } | CurveKind::Circle { .. }))
69 .map(|(&id, _)| {
70 n += 1;
71 (id, n - 1)
72 })
73 .collect();
74 Layout {
75 point_var,
76 curve_var,
77 n,
78 }
79 }
80
81 fn read(&self, sketch: &Sketch) -> Vec<f64> {
83 let mut x = vec![0.0; self.n];
84 for (id, p) in sketch.points.iter().rev() {
87 x[self.point_var[id]] = p.x;
88 x[self.point_var[id] + 1] = p.y;
89 }
90 for (id, c) in &sketch.curves {
91 match c.kind {
92 CurveKind::Arc { sweep, .. } => x[self.curve_var[id]] = sweep / 2.0,
93 CurveKind::Circle { radius, .. } => x[self.curve_var[id]] = radius,
94 _ => {}
95 }
96 }
97 x
98 }
99
100 fn write(&self, sketch: &mut Sketch, x: &[f64]) {
102 for (id, p) in sketch.points.iter_mut() {
103 p.x = x[self.point_var[id]];
104 p.y = x[self.point_var[id] + 1];
105 }
106 for (id, c) in sketch.curves.iter_mut() {
107 match &mut c.kind {
108 CurveKind::Arc { sweep, .. } => *sweep = 2.0 * x[self.curve_var[id]],
109 CurveKind::Circle { radius, .. } => *radius = x[self.curve_var[id]].abs(),
110 _ => {}
111 }
112 }
113 }
114
115 fn curve_vars(&self, sketch: &Sketch, c: CurveId, out: &mut Vec<usize>) {
117 for p in sketch.curves[&c].points() {
118 out.extend([self.point_var[&p], self.point_var[&p] + 1]);
119 }
120 out.extend(self.curve_var.get(&c));
121 }
122}
123
124struct Geo<'a, T> {
127 sketch: &'a Sketch,
128 layout: &'a Layout,
129 x: &'a [f64],
130 seeded: &'a [(usize, T)],
131}
132
133impl<T: Scalar> Geo<'_, T> {
134 fn var(&self, i: usize) -> T {
135 self.seeded
136 .iter()
137 .find(|(j, _)| *j == i)
138 .map(|(_, t)| *t)
139 .unwrap_or_else(|| T::from_f64(self.x[i]))
140 }
141 fn point(&self, p: PointId) -> V<T> {
142 let i = self.layout.point_var[&p];
143 V::new(self.var(i), self.var(i + 1))
144 }
145 fn line(&self, c: CurveId) -> (V<T>, V<T>) {
146 match &self.sketch.curves[&c].kind {
147 CurveKind::Line { start, end } => (self.point(*start), self.point(*end)),
148 k => unreachable!("validated to be a line: {k:?}"),
149 }
150 }
151 fn arc(&self, c: CurveId) -> Option<Arc<T>> {
152 match &self.sketch.curves[&c].kind {
153 CurveKind::Arc { start, end, .. } => Some(Arc {
154 s: self.point(*start),
155 e: self.point(*end),
156 half: self.var(self.layout.curve_var[&c]),
157 }),
158 _ => None,
159 }
160 }
161 fn round(&self, c: CurveId) -> GeopResult<(V<T>, T)> {
163 Ok(match &self.sketch.curves[&c].kind {
164 CurveKind::Circle { center, .. } => (
165 self.point(*center),
166 self.var(self.layout.curve_var[&c]).abs(),
167 ),
168 CurveKind::Arc { .. } => {
169 let a = self.arc(c).unwrap();
170 (a.center()?, a.radius()?)
171 }
172 k => unreachable!("validated to be a circle or arc: {k:?}"),
173 })
174 }
175 fn end_tangent(&self, c: CurveId, at_end: bool) -> GeopResult<V<T>> {
178 Ok(match &self.sketch.curves[&c].kind {
179 CurveKind::Line { .. } => {
180 let (s, e) = self.line(c);
181 e.sub(s).unit()?
182 }
183 CurveKind::Arc { .. } => {
184 let a = self.arc(c).unwrap();
185 if at_end {
186 a.tangent_end()?
187 } else {
188 a.tangent_start()?
189 }
190 }
191 CurveKind::Spline { control_points } => {
192 let n = control_points.len();
193 let (p, q) = if at_end {
194 (control_points[n - 2], control_points[n - 1])
195 } else {
196 (control_points[0], control_points[1])
197 };
198 self.point(q).sub(self.point(p)).unit()?
199 }
200 k => unreachable!("validated to be an open curve: {k:?}"),
201 })
202 }
203}
204
205#[derive(Clone, Copy, Debug)]
208enum TangentMode {
209 Endpoint { a_end: bool, b_end: bool },
211 LineRound { line: CurveId, round: CurveId },
213 RoundRound { internal: bool },
217}
218
219struct Prepared<'a> {
221 constraint: &'a Constraint,
222 vars: Vec<usize>,
223 tangent: Option<TangentMode>,
224}
225
226struct Problem<'a> {
228 sketch: &'a Sketch,
229 layout: Layout,
230 constraints: Vec<Prepared<'a>>,
231 scale: f64,
233 drags: Vec<(PointId, [f64; 2], f64)>,
236}
237
238impl<'a> Problem<'a> {
239 fn new(sketch: &'a Sketch) -> GeopResult<Self> {
240 sketch.validate()?;
241 let layout = Layout::new(sketch);
242 let x0 = layout.read(sketch);
243
244 let mut lo = [f64::INFINITY; 2];
245 let mut hi = [f64::NEG_INFINITY; 2];
246 for p in sketch.points.values() {
247 lo = [lo[0].min(p.x), lo[1].min(p.y)];
248 hi = [hi[0].max(p.x), hi[1].max(p.y)];
249 }
250 let diagonal = (hi[0] - lo[0]).hypot(hi[1] - lo[1]);
251 let radii = sketch.curves.values().filter_map(|c| match c.kind {
252 CurveKind::Circle { radius, .. } => Some(2.0 * radius.abs()),
253 _ => None,
254 });
255 let scale = radii.fold(diagonal, f64::max);
256 let scale = if scale.is_finite() && scale > 0.0 {
257 scale
258 } else {
259 1.0
260 };
261
262 let mut constraints = Vec::new();
263 for c in sketch.constraints.values() {
264 use Constraint::*;
265 let mut vars = Vec::new();
266 let pt = |p: &PointId, vars: &mut Vec<usize>| {
267 vars.extend([layout.point_var[p], layout.point_var[p] + 1])
268 };
269 match c {
270 Coincident { .. } => continue,
272 Fix { point, .. } => pt(point, &mut vars),
273 Distance { a, b, .. } | DistanceX { a, b, .. } | DistanceY { a, b, .. } => {
274 pt(a, &mut vars);
275 pt(b, &mut vars);
276 }
277 PointOnCurve { point, curve }
278 | Midpoint { point, curve }
279 | PointLineDistance {
280 point, line: curve, ..
281 } => {
282 pt(point, &mut vars);
283 layout.curve_vars(sketch, *curve, &mut vars);
284 }
285 Symmetric { a, b, line } => {
286 pt(a, &mut vars);
287 pt(b, &mut vars);
288 layout.curve_vars(sketch, *line, &mut vars);
289 }
290 Horizontal { line: curve }
291 | Vertical { line: curve }
292 | Length { curve, .. }
293 | Radius { curve, .. } => layout.curve_vars(sketch, *curve, &mut vars),
294 Parallel { a, b }
295 | Perpendicular { a, b }
296 | Collinear { a, b }
297 | Tangent { a, b }
298 | Equal { a, b }
299 | Concentric { a, b }
300 | Angle { a, b, .. } => {
301 layout.curve_vars(sketch, *a, &mut vars);
302 layout.curve_vars(sketch, *b, &mut vars);
303 }
304 }
305 vars.sort_unstable();
306 vars.dedup();
307 if vars.len() > MAX_LOCAL_VARS {
308 return Err(GeopError::new(format!(
309 "constraint {c:?} depends on {} variables, more than the supported {MAX_LOCAL_VARS}",
310 vars.len()
311 )));
312 }
313 let tangent = match c {
314 Tangent { a, b } => Some(Self::tangent_mode(sketch, &layout, &x0, *a, *b)?),
315 _ => None,
316 };
317 constraints.push(Prepared {
318 constraint: c,
319 vars,
320 tangent,
321 });
322 }
323
324 Ok(Problem {
325 sketch,
326 layout,
327 constraints,
328 scale,
329 drags: Vec::new(),
330 })
331 }
332
333 fn tangent_mode(
334 sketch: &Sketch,
335 layout: &Layout,
336 x: &[f64],
337 a: CurveId,
338 b: CurveId,
339 ) -> GeopResult<TangentMode> {
340 if let Some((a_end, b_end)) = sketch.shared_endpoint(a, b)? {
341 return Ok(TangentMode::Endpoint { a_end, b_end });
342 }
343 let is_line = |c: CurveId| matches!(sketch.curves[&c].kind, CurveKind::Line { .. });
344 if is_line(a) {
345 return Ok(TangentMode::LineRound { line: a, round: b });
346 }
347 if is_line(b) {
348 return Ok(TangentMode::LineRound { line: b, round: a });
349 }
350 let geo = Geo::<S> {
351 sketch,
352 layout,
353 x,
354 seeded: &[],
355 };
356 let ((ca, ra), (cb, rb)) = (geo.round(a)?, geo.round(b)?);
357 let d = ca.sub(cb).norm()?.to_f64();
358 let (ra, rb) = (ra.to_f64(), rb.to_f64());
359 Ok(TangentMode::RoundRound {
360 internal: (d - (ra - rb).abs()).abs() < (d - (ra + rb)).abs(),
361 })
362 }
363
364 fn residuals<T: Scalar>(&self, p: &Prepared, geo: &Geo<T>, out: &mut Vec<T>) -> GeopResult<()> {
365 use Constraint::*;
366 let scale = T::from_f64(self.scale);
367 match *p.constraint {
368 Coincident { .. } => {}
369 Fix { point, x, y } => {
370 let q = geo.point(point);
371 out.extend([q.x.sub(T::from_f64(x)), q.y.sub(T::from_f64(y))]);
372 }
373 Distance { a, b, value } => out.push(
374 geo.point(b)
375 .sub(geo.point(a))
376 .norm()?
377 .sub(T::from_f64(value)),
378 ),
379 DistanceX { a, b, value } => {
380 out.push(geo.point(b).x.sub(geo.point(a).x).sub(T::from_f64(value)))
381 }
382 DistanceY { a, b, value } => {
383 out.push(geo.point(b).y.sub(geo.point(a).y).sub(T::from_f64(value)))
384 }
385 PointOnCurve { point, curve } => {
386 let q = geo.point(point);
387 out.push(match &self.sketch.curves[&curve].kind {
388 CurveKind::Line { .. } => {
389 let (s, e) = geo.line(curve);
390 line_distance(s, e, q)?
391 }
392 CurveKind::Arc { .. } => geo.arc(curve).unwrap().circle_residual(q)?,
393 _ => {
394 let (c, r) = geo.round(curve)?;
395 q.sub(c).norm()?.sub(r)
396 }
397 });
398 }
399 Midpoint { point, curve } => {
400 let q = geo.point(point);
401 let m = match geo.arc(curve) {
402 Some(a) => a.arc_mid()?,
403 None => {
404 let (s, e) = geo.line(curve);
405 s.add(e).scale(T::from_f64(0.5))
406 }
407 };
408 out.extend([q.x.sub(m.x), q.y.sub(m.y)]);
409 }
410 Symmetric { a, b, line } => {
411 let (pa, pb) = (geo.point(a), geo.point(b));
412 let (s, e) = geo.line(line);
413 let mid = pa.add(pb).scale(T::from_f64(0.5));
414 out.push(line_distance(s, e, mid)?);
415 out.push(pb.sub(pa).dot(e.sub(s).unit()?));
416 }
417 PointLineDistance { point, line, value } => {
418 let (s, e) = geo.line(line);
419 out.push(
420 line_distance(s, e, geo.point(point))?
421 .abs()
422 .sub(T::from_f64(value)),
423 );
424 }
425 Horizontal { line } => {
426 let (s, e) = geo.line(line);
427 out.push(e.y.sub(s.y));
428 }
429 Vertical { line } => {
430 let (s, e) = geo.line(line);
431 out.push(e.x.sub(s.x));
432 }
433 Parallel { a, b } | Perpendicular { a, b } | Angle { a, b, .. } => {
434 let (sa, ea) = geo.line(a);
435 let (sb, eb) = geo.line(b);
436 let (ua, ub) = (ea.sub(sa).unit()?, eb.sub(sb).unit()?);
437 let (cross, dot) = (ua.cross(ub), ua.dot(ub));
438 let r = match *p.constraint {
440 Parallel { .. } => cross,
441 Perpendicular { .. } => dot,
442 Angle { value, .. } => cross
443 .mul(T::from_f64(value.cos()))
444 .sub(dot.mul(T::from_f64(value.sin()))),
445 _ => unreachable!(),
446 };
447 out.push(r.mul(scale));
448 }
449 Collinear { a, b } => {
450 let (sa, ea) = geo.line(a);
451 let (sb, eb) = geo.line(b);
452 out.push(line_distance(sa, ea, sb)?);
453 out.push(line_distance(sa, ea, eb)?);
454 }
455 Tangent { a, b } => match p.tangent.unwrap() {
456 TangentMode::Endpoint { a_end, b_end } => {
457 let (ta, tb) = (geo.end_tangent(a, a_end)?, geo.end_tangent(b, b_end)?);
458 out.push(ta.cross(tb).mul(scale));
459 }
460 TangentMode::LineRound { line, round } => {
461 let (s, e) = geo.line(line);
462 let (c, r) = geo.round(round)?;
463 out.push(line_distance(s, e, c)?.abs().sub(r));
464 }
465 TangentMode::RoundRound { internal } => {
466 let ((ca, ra), (cb, rb)) = (geo.round(a)?, geo.round(b)?);
467 let d = ca.sub(cb).norm()?;
468 out.push(if internal {
469 d.sub(ra.sub(rb).abs())
470 } else {
471 d.sub(ra.add(rb))
472 });
473 }
474 },
475 Equal { a, b } => {
476 let size = |c: CurveId| -> GeopResult<T> {
477 Ok(match &self.sketch.curves[&c].kind {
478 CurveKind::Line { .. } => {
479 let (s, e) = geo.line(c);
480 e.sub(s).norm()?
481 }
482 _ => geo.round(c)?.1,
483 })
484 };
485 out.push(size(a)?.sub(size(b)?));
486 }
487 Concentric { a, b } => {
488 let (ca, cb) = (geo.round(a)?.0, geo.round(b)?.0);
489 out.extend([ca.x.sub(cb.x), ca.y.sub(cb.y)]);
490 }
491 Length { curve, value } => {
492 let length = match geo.arc(curve) {
493 Some(a) => a.length()?,
494 None => {
495 let (s, e) = geo.line(curve);
496 e.sub(s).norm()?
497 }
498 };
499 out.push(length.sub(T::from_f64(value)));
500 }
501 Radius { curve, value } => out.push(match geo.arc(curve) {
502 Some(a) => T::from_f64(2.0 * value)
505 .mul(a.half.sin().abs())
506 .sub(a.chord_length()?),
507 None => geo.round(curve)?.1.sub(T::from_f64(value)),
508 }),
509 }
510 Ok(())
511 }
512
513 fn objective(&self, x: &[f64]) -> (f64, Vec<f64>) {
518 let mut f = 0.0;
519 let mut g = vec![0.0; x.len()];
520 let mut rs = Vec::new();
521 for p in &self.constraints {
522 let seeded: Vec<(usize, Dual<S>)> = p
523 .vars
524 .iter()
525 .enumerate()
526 .map(|(slot, &i)| (i, Dual::var(S::from_f64(x[i]), slot)))
527 .collect();
528 let geo = Geo {
529 sketch: self.sketch,
530 layout: &self.layout,
531 x,
532 seeded: &seeded,
533 };
534 rs.clear();
535 if self.residuals(p, &geo, &mut rs).is_err() {
536 return (f64::INFINITY, vec![0.0; x.len()]);
537 }
538 for r in &rs {
539 let rv = r.v.to_f64();
540 f += rv * rv;
541 for (slot, &i) in p.vars.iter().enumerate() {
542 g[i] += 2.0 * rv * r.d[slot].to_f64();
543 }
544 }
545 }
546 for &(point, target, weight) in &self.drags {
547 let i = self.layout.point_var[&point];
548 for k in 0..2 {
549 let r = weight * (x[i + k] - target[k]);
550 f += r * r;
551 g[i + k] += 2.0 * weight * r;
552 }
553 }
554 (f, g)
555 }
556
557 fn jacobian(&self, x: &[f64]) -> (Vec<f64>, Vec<Vec<f64>>) {
562 let mut values = Vec::new();
563 let mut rows = Vec::new();
564 let mut rs = Vec::new();
565 for p in &self.constraints {
566 let seeded: Vec<(usize, Dual<S>)> = p
567 .vars
568 .iter()
569 .enumerate()
570 .map(|(slot, &i)| (i, Dual::var(S::from_f64(x[i]), slot)))
571 .collect();
572 let geo = Geo {
573 sketch: self.sketch,
574 layout: &self.layout,
575 x,
576 seeded: &seeded,
577 };
578 rs.clear();
579 if self.residuals(p, &geo, &mut rs).is_err() {
580 continue;
581 }
582 for r in &rs {
583 values.push(r.v.to_f64());
584 let mut row = vec![0.0; x.len()];
585 for (slot, &i) in p.vars.iter().enumerate() {
586 row[i] += r.d[slot].to_f64();
587 }
588 rows.push(row);
589 }
590 }
591 (values, rows)
592 }
593
594 fn minimize(&self, x: Vec<f64>, max_iterations: usize) -> (Vec<f64>, usize) {
595 let tol = RELATIVE_TOLERANCE * self.scale;
596 let r = minimize(
597 |x| self.objective(x),
598 x,
599 BfgsOptions {
600 max_iterations,
601 f_tolerance: (0.01 * tol).powi(2),
602 g_tolerance: 0.0,
603 },
604 );
605 (r.x, r.iterations)
606 }
607}
608
609const RELATIVE_TOLERANCE: f64 = 1e-9;
612
613#[derive(Clone, Debug, PartialEq, Serialize)]
615pub struct SolveReport {
616 pub converged: bool,
618 pub max_residual: f64,
620 pub iterations: usize,
621 pub dof: usize,
623 pub free_points: BTreeMap<PointId, bool>,
625 pub free_curves: BTreeMap<CurveId, bool>,
628 pub failed_constraints: Vec<ConstraintId>,
631}
632
633impl Sketch {
634 pub fn solve(&mut self) -> GeopResult<SolveReport> {
641 self.solve_with_drag(&[])
642 }
643
644 pub fn solve_with_drag(&mut self, drags: &[(PointId, [f64; 2])]) -> GeopResult<SolveReport> {
647 let mut problem = Problem::new(self)?;
648 let x0 = problem.layout.read(self);
649 let mut iterations = 0;
650 let mut x = x0;
651 if !drags.is_empty() {
652 problem.drags = drags.iter().map(|&(p, t)| (p, t, 0.1)).collect();
656 let (x1, it) = problem.minimize(x, 200);
657 problem.drags.clear();
658 x = x1;
659 iterations += it;
660 }
661 let (x, it) = problem.minimize(x, 2000);
662 iterations += it;
663
664 let report = problem.report(&x, iterations);
665 let layout = problem.layout;
666 layout.write(self, &x);
667 Ok(report)
668 }
669}
670
671impl Problem<'_> {
672 fn report(&self, x: &[f64], iterations: usize) -> SolveReport {
673 let tol = RELATIVE_TOLERANCE * self.scale;
674 let (values, rows) = self.jacobian(x);
675 let max_residual = values.iter().fold(0.0f64, |m, r| m.max(r.abs()));
676
677 let mut failed_constraints = Vec::new();
678 let mut rs = Vec::new();
679 let mut prepared = self.constraints.iter();
680 for (&i, c) in &self.sketch.constraints {
681 if matches!(c, Constraint::Coincident { .. }) {
682 continue;
683 }
684 let p = prepared.next().unwrap();
685 let geo = Geo::<S> {
686 sketch: self.sketch,
687 layout: &self.layout,
688 x,
689 seeded: &[],
690 };
691 rs.clear();
692 if self.residuals(p, &geo, &mut rs).is_err() {
693 failed_constraints.push(i);
694 continue;
695 }
696 if rs.iter().any(|r| !r.is_finite() || r.to_f64().abs() > tol) {
697 failed_constraints.push(i);
698 }
699 }
700
701 let free_vars = free_variables(rows, self.layout.n);
702 let dof = free_vars.1;
703 let free_vars = free_vars.0;
704 let free_points = self
705 .layout
706 .point_var
707 .iter()
708 .map(|(&p, &i)| (p, free_vars[i] || free_vars[i + 1]))
709 .collect();
710 let free_curves = self
711 .sketch
712 .curves
713 .keys()
714 .map(|&c| {
715 let mut vars = Vec::new();
716 self.layout.curve_vars(self.sketch, c, &mut vars);
717 (c, vars.iter().any(|&i| free_vars[i]))
718 })
719 .collect();
720
721 SolveReport {
722 converged: failed_constraints.is_empty(),
723 max_residual,
724 iterations,
725 dof,
726 free_points,
727 free_curves,
728 failed_constraints,
729 }
730 }
731}
732
733fn free_variables(mut rows: Vec<Vec<f64>>, n: usize) -> (Vec<bool>, usize) {
741 let largest = rows.iter().flatten().fold(0.0f64, |m, v| m.max(v.abs()));
742 let zero = 1e-9 * largest.max(1e-300);
743 let mut pivot_cols = Vec::new();
744 let mut r = 0;
745 for col in 0..n {
746 let Some(best) =
747 (r..rows.len()).max_by(|&a, &b| rows[a][col].abs().total_cmp(&rows[b][col].abs()))
748 else {
749 break;
750 };
751 if rows[best][col].abs() <= zero {
752 continue;
753 }
754 rows.swap(r, best);
755 let pivot = rows[r][col];
756 for v in &mut rows[r] {
757 *v /= pivot;
758 }
759 for i in 0..rows.len() {
760 if i != r && rows[i][col] != 0.0 {
761 let factor = rows[i][col];
762 let (pivot_row, row) = if i < r {
763 let (lo, hi) = rows.split_at_mut(r);
764 (&hi[0], &mut lo[i])
765 } else {
766 let (lo, hi) = rows.split_at_mut(i);
767 (&lo[r], &mut hi[0])
768 };
769 for (v, p) in row.iter_mut().zip(pivot_row) {
770 *v -= factor * p;
771 }
772 }
773 }
774 pivot_cols.push(col);
775 r += 1;
776 }
777
778 let mut free = vec![false; n];
781 let is_pivot: Vec<bool> = (0..n).map(|c| pivot_cols.contains(&c)).collect();
782 for f in (0..n).filter(|&c| !is_pivot[c]) {
783 free[f] = true;
784 for (i, &pc) in pivot_cols.iter().enumerate() {
785 if rows[i][f].abs() > 1e-7 {
786 free[pc] = true;
787 }
788 }
789 }
790 (free, n - pivot_cols.len())
791}