1use std::collections::HashMap;
4
5use crate::SketchError;
6
7use super::constraint::{
8 Constraint, ConstraintEntry, ConstraintId, EntitySnapshot, JacobianWriter, eval_jacobian,
9 eval_residuals, residual_count,
10};
11use super::dof::{self, DofAnalysis};
12use super::entity::{
13 ArcData, ArcId, CircleData, CircleId, GenArena, LineData, LineId, ParamRef, PointData, PointId,
14};
15use super::solver::{self, SolveResult};
16
17pub struct GcsSystem {
22 points: GenArena<PointData>,
23 lines: GenArena<LineData>,
24 circles: GenArena<CircleData>,
25 arcs: GenArena<ArcData>,
26 constraints: GenArena<ConstraintEntry>,
27 arc_internal_constraints: HashMap<ArcId, ConstraintId>,
30 param_map: Vec<ParamRef>,
32 param_index: HashMap<ParamRef, usize>,
34 dirty: bool,
36}
37
38impl Clone for GcsSystem {
39 fn clone(&self) -> Self {
40 Self {
41 points: self.points.clone(),
42 lines: self.lines.clone(),
43 circles: self.circles.clone(),
44 arcs: self.arcs.clone(),
45 constraints: self.constraints.clone(),
46 arc_internal_constraints: self.arc_internal_constraints.clone(),
47 param_map: self.param_map.clone(),
48 param_index: self.param_index.clone(),
49 dirty: self.dirty,
50 }
51 }
52}
53
54impl Default for GcsSystem {
55 fn default() -> Self {
56 Self::new()
57 }
58}
59
60impl GcsSystem {
61 #[must_use]
63 pub fn new() -> Self {
64 Self {
65 points: GenArena::new(),
66 lines: GenArena::new(),
67 circles: GenArena::new(),
68 arcs: GenArena::new(),
69 constraints: GenArena::new(),
70 arc_internal_constraints: HashMap::new(),
71 param_map: Vec::new(),
72 param_index: HashMap::new(),
73 dirty: false,
74 }
75 }
76
77 pub fn add_point(&mut self, data: PointData) -> PointId {
79 self.dirty = true;
80 self.points.insert(data)
81 }
82
83 #[must_use]
85 pub fn point(&self, id: PointId) -> Option<&PointData> {
86 self.points.get(id)
87 }
88
89 pub fn point_mut(&mut self, id: PointId) -> Option<&mut PointData> {
91 self.points.get_mut(id)
92 }
93
94 pub fn remove_point(&mut self, id: PointId) -> Result<PointData, SketchError> {
102 for (_, line) in self.lines.iter() {
103 if line.p1 == id || line.p2 == id {
104 return Err(SketchError::EntityInUse);
105 }
106 }
107 for (_, circle) in self.circles.iter() {
108 if circle.center == id {
109 return Err(SketchError::EntityInUse);
110 }
111 }
112 for (_, arc) in self.arcs.iter() {
113 if arc.center == id || arc.start == id || arc.end == id {
114 return Err(SketchError::EntityInUse);
115 }
116 }
117 for (_, entry) in self.constraints.iter() {
118 if constraint_references_point(&entry.constraint, id) {
119 return Err(SketchError::EntityInUse);
120 }
121 }
122 self.dirty = true;
123 self.points.remove(id).ok_or(SketchError::InvalidHandle)
124 }
125
126 pub fn add_line(&mut self, p1: PointId, p2: PointId) -> Result<LineId, SketchError> {
132 if !self.points.contains(p1) || !self.points.contains(p2) {
133 return Err(SketchError::InvalidHandle);
134 }
135 Ok(self.lines.insert(LineData { p1, p2 }))
136 }
137
138 #[must_use]
140 pub fn line(&self, id: LineId) -> Option<&LineData> {
141 self.lines.get(id)
142 }
143
144 pub fn remove_line(&mut self, id: LineId) -> Result<LineData, SketchError> {
151 for (_, entry) in self.constraints.iter() {
152 if constraint_references_line(&entry.constraint, id) {
153 return Err(SketchError::EntityInUse);
154 }
155 }
156 self.lines.remove(id).ok_or(SketchError::InvalidHandle)
157 }
158
159 pub fn add_circle(&mut self, center: PointId, radius: f64) -> Result<CircleId, SketchError> {
165 if !self.points.contains(center) {
166 return Err(SketchError::InvalidHandle);
167 }
168 self.dirty = true;
169 Ok(self.circles.insert(CircleData { center, radius }))
170 }
171
172 #[must_use]
174 pub fn circle(&self, id: CircleId) -> Option<&CircleData> {
175 self.circles.get(id)
176 }
177
178 pub fn remove_circle(&mut self, id: CircleId) -> Result<CircleData, SketchError> {
185 for (_, entry) in self.constraints.iter() {
186 if constraint_references_circle(&entry.constraint, id) {
187 return Err(SketchError::EntityInUse);
188 }
189 }
190 self.dirty = true;
191 self.circles.remove(id).ok_or(SketchError::InvalidHandle)
192 }
193
194 pub fn add_arc(
204 &mut self,
205 center: PointId,
206 start: PointId,
207 end: PointId,
208 ) -> Result<ArcId, SketchError> {
209 self.check_point(center)?;
210 self.check_point(start)?;
211 self.check_point(end)?;
212
213 let arc_id = self.arcs.insert(ArcData { center, start, end });
214
215 let cid = self.constraints.insert(ConstraintEntry {
218 constraint: Constraint::PointOnArc(end, arc_id),
219 });
220 self.arc_internal_constraints.insert(arc_id, cid);
221
222 self.dirty = true;
223 Ok(arc_id)
224 }
225
226 #[must_use]
228 pub fn arc(&self, id: ArcId) -> Option<&ArcData> {
229 self.arcs.get(id)
230 }
231
232 pub fn arc_mut(&mut self, id: ArcId) -> Option<&mut ArcData> {
234 self.arcs.get_mut(id)
235 }
236
237 pub fn remove_arc(&mut self, id: ArcId) -> Result<ArcData, SketchError> {
247 for (cid, entry) in self.constraints.iter() {
248 if self.arc_internal_constraints.get(&id) == Some(&cid) {
249 continue;
250 }
251 if constraint_references_arc(&entry.constraint, id) {
252 return Err(SketchError::EntityInUse);
253 }
254 }
255
256 if let Some(cid) = self.arc_internal_constraints.remove(&id) {
257 self.constraints.remove(cid);
258 }
259
260 self.dirty = true;
261 self.arcs.remove(id).ok_or(SketchError::InvalidHandle)
262 }
263
264 #[must_use]
266 pub fn arc_count(&self) -> usize {
267 self.arcs.len()
268 }
269
270 pub fn arcs(&self) -> impl Iterator<Item = (ArcId, &ArcData)> {
272 self.arcs.iter()
273 }
274
275 pub fn add_constraint(&mut self, constraint: Constraint) -> Result<ConstraintId, SketchError> {
282 self.validate_constraint(&constraint)?;
283 self.dirty = true;
284 Ok(self.constraints.insert(ConstraintEntry { constraint }))
285 }
286
287 pub fn remove_constraint(&mut self, id: ConstraintId) -> Result<(), SketchError> {
293 self.constraints
294 .remove(id)
295 .map(|_| {
296 self.dirty = true;
297 })
298 .ok_or(SketchError::InvalidHandle)
299 }
300
301 #[must_use]
303 pub fn constraint(&self, id: ConstraintId) -> Option<&Constraint> {
304 self.constraints.get(id).map(|e| &e.constraint)
305 }
306
307 #[must_use]
309 pub fn constraint_count(&self) -> usize {
310 self.constraints.len()
311 }
312
313 #[must_use]
315 pub fn point_count(&self) -> usize {
316 self.points.len()
317 }
318
319 #[must_use]
321 pub fn line_count(&self) -> usize {
322 self.lines.len()
323 }
324
325 #[must_use]
327 pub fn circle_count(&self) -> usize {
328 self.circles.len()
329 }
330
331 #[allow(clippy::unnecessary_wraps)]
341 pub fn solve(
342 &mut self,
343 max_iterations: usize,
344 tolerance: f64,
345 ) -> Result<SolveResult, SketchError> {
346 self.rebuild_if_dirty();
347
348 let n = self.param_map.len();
349 let m: usize = self
350 .constraints
351 .iter()
352 .map(|(_, e)| residual_count(&e.constraint))
353 .sum();
354
355 if n == 0 {
356 let snap = self.build_snapshot();
358 let mut residuals = Vec::with_capacity(m);
359 for (_, entry) in self.constraints.iter() {
360 eval_residuals(&entry.constraint, &snap, &mut residuals);
361 }
362 let max_r = residuals.iter().fold(0.0_f64, |a, &b| a.max(b.abs()));
363 return Ok(SolveResult {
364 converged: max_r < tolerance,
365 iterations: 0,
366 max_residual: max_r,
367 });
368 }
369
370 let mut params = self.extract_params();
371 let param_index = self.param_index.clone();
372 let param_map = self.param_map.clone();
373
374 let constraints: Vec<Constraint> = self
375 .constraints
376 .iter()
377 .map(|(_, e)| e.constraint.clone())
378 .collect();
379
380 let residual_fn = |p: &[f64]| -> Vec<f64> {
381 let snap = build_snapshot_from_params(p, ¶m_map, ¶m_index, self);
382 let mut r = Vec::with_capacity(m);
383 for c in &constraints {
384 eval_residuals(c, &snap, &mut r);
385 }
386 r
387 };
388
389 let jacobian_fn = |p: &[f64]| -> Vec<f64> {
390 let snap = build_snapshot_from_params(p, ¶m_map, ¶m_index, self);
391 let mut jac = vec![0.0; m * n];
392 let mut row = 0;
393 {
394 let mut jw = JacobianWriter {
395 data: &mut jac,
396 ncols: n,
397 param_index: ¶m_index,
398 };
399 for c in &constraints {
400 eval_jacobian(c, &snap, &mut jw, row);
401 row += residual_count(c);
402 }
403 }
404 jac
405 };
406
407 let result = solver::solve_dogleg(
408 &mut params,
409 &residual_fn,
410 &jacobian_fn,
411 m,
412 max_iterations,
413 tolerance,
414 );
415
416 self.write_params(¶ms);
417
418 Ok(result)
419 }
420
421 pub fn dof(&mut self) -> DofAnalysis {
423 self.rebuild_if_dirty();
424
425 let n = self.param_map.len();
426 let m: usize = self
427 .constraints
428 .iter()
429 .map(|(_, e)| residual_count(&e.constraint))
430 .sum();
431
432 if n == 0 || m == 0 {
433 return DofAnalysis {
434 dof: n,
435 rank: 0,
436 num_params: n,
437 num_equations: m,
438 };
439 }
440
441 let params = self.extract_params();
442 let snap = self.build_snapshot();
443 let mut jac = vec![0.0; m * n];
444 let mut row = 0;
445 {
446 let mut jw = JacobianWriter {
447 data: &mut jac,
448 ncols: n,
449 param_index: &self.param_index,
450 };
451 for (_, entry) in self.constraints.iter() {
452 eval_jacobian(&entry.constraint, &snap, &mut jw, row);
453 row += residual_count(&entry.constraint);
454 }
455 }
456 let _ = params; dof::analyze(&jac, m, n)
459 }
460
461 pub fn points(&self) -> impl Iterator<Item = (PointId, &PointData)> {
463 self.points.iter()
464 }
465
466 pub fn lines(&self) -> impl Iterator<Item = (LineId, &LineData)> {
468 self.lines.iter()
469 }
470
471 pub fn circles(&self) -> impl Iterator<Item = (CircleId, &CircleData)> {
473 self.circles.iter()
474 }
475
476 fn rebuild_if_dirty(&mut self) {
478 if !self.dirty {
479 return;
480 }
481 self.param_map.clear();
482 self.param_index.clear();
483
484 for (id, data) in self.points.iter() {
485 if !data.fixed {
486 let idx = self.param_map.len();
487 self.param_map.push(ParamRef::PointX(id));
488 self.param_index.insert(ParamRef::PointX(id), idx);
489 let idx = self.param_map.len();
490 self.param_map.push(ParamRef::PointY(id));
491 self.param_index.insert(ParamRef::PointY(id), idx);
492 }
493 }
494
495 for (id, _) in self.circles.iter() {
496 let idx = self.param_map.len();
497 self.param_map.push(ParamRef::CircleRadius(id));
498 self.param_index.insert(ParamRef::CircleRadius(id), idx);
499 }
500
501 self.dirty = false;
502 }
503
504 fn extract_params(&self) -> Vec<f64> {
506 self.param_map
507 .iter()
508 .map(|pr| match pr {
509 ParamRef::PointX(id) => self.points.get(*id).map_or(0.0, |p| p.x),
510 ParamRef::PointY(id) => self.points.get(*id).map_or(0.0, |p| p.y),
511 ParamRef::CircleRadius(id) => self.circles.get(*id).map_or(0.0, |c| c.radius),
512 })
513 .collect()
514 }
515
516 fn write_params(&mut self, params: &[f64]) {
518 for (i, pr) in self.param_map.iter().enumerate() {
519 match pr {
520 ParamRef::PointX(id) => {
521 if let Some(p) = self.points.get_mut(*id) {
522 p.x = params[i];
523 }
524 }
525 ParamRef::PointY(id) => {
526 if let Some(p) = self.points.get_mut(*id) {
527 p.y = params[i];
528 }
529 }
530 ParamRef::CircleRadius(id) => {
531 if let Some(c) = self.circles.get_mut(*id) {
532 c.radius = params[i];
533 }
534 }
535 }
536 }
537 }
538
539 fn build_snapshot(&self) -> EntitySnapshot {
541 EntitySnapshot {
542 points: self.points.iter().map(|(id, d)| (id, (d.x, d.y))).collect(),
543 lines: self
544 .lines
545 .iter()
546 .map(|(id, d)| (id, (d.p1, d.p2)))
547 .collect(),
548 circles: self
549 .circles
550 .iter()
551 .map(|(id, d)| (id, (d.center, d.radius)))
552 .collect(),
553 arcs: self
554 .arcs
555 .iter()
556 .map(|(id, d)| (id, (d.center, d.start, d.end)))
557 .collect(),
558 }
559 }
560
561 fn validate_constraint(&self, c: &Constraint) -> Result<(), SketchError> {
563 match c {
564 Constraint::Coincident(p1, p2) | Constraint::Distance(p1, p2, _) => {
565 self.check_point(*p1)?;
566 self.check_point(*p2)?;
567 }
568 Constraint::PointLineDistance(pt, line, _) => {
569 self.check_point(*pt)?;
570 self.check_line(*line)?;
571 }
572 Constraint::FixX(p, _) | Constraint::FixY(p, _) => {
573 self.check_point(*p)?;
574 }
575 Constraint::Horizontal(line) | Constraint::Vertical(line) => {
576 self.check_line(*line)?;
577 }
578 Constraint::Angle(l1, l2, _)
579 | Constraint::Perpendicular(l1, l2)
580 | Constraint::Parallel(l1, l2) => {
581 self.check_line(*l1)?;
582 self.check_line(*l2)?;
583 }
584 Constraint::PointOnCircle(pt, circ) => {
585 self.check_point(*pt)?;
586 self.check_circle(*circ)?;
587 }
588 Constraint::PointOnArc(pt, arc) => {
589 self.check_point(*pt)?;
590 self.check_arc(*arc)?;
591 }
592 Constraint::TangentLineArc(line, arc, shared) => {
593 self.check_line(*line)?;
594 self.check_arc(*arc)?;
595 self.check_point(*shared)?;
596 }
597 Constraint::TangentArcArc(arc1, arc2, shared) => {
598 self.check_arc(*arc1)?;
599 self.check_arc(*arc2)?;
600 self.check_point(*shared)?;
601 }
602 Constraint::EqualRadiusArcArc(arc1, arc2) => {
603 self.check_arc(*arc1)?;
604 self.check_arc(*arc2)?;
605 }
606 Constraint::EqualRadiusArcCircle(arc, circ) => {
607 self.check_arc(*arc)?;
608 self.check_circle(*circ)?;
609 }
610 Constraint::ArcLength(arc, _) => {
611 self.check_arc(*arc)?;
612 }
613 Constraint::ConcentricArcArc(arc1, arc2) => {
614 self.check_arc(*arc1)?;
615 self.check_arc(*arc2)?;
616 }
617 Constraint::ConcentricArcCircle(arc, circ) => {
618 self.check_arc(*arc)?;
619 self.check_circle(*circ)?;
620 }
621 }
622 Ok(())
623 }
624
625 fn check_point(&self, id: PointId) -> Result<(), SketchError> {
626 if self.points.contains(id) {
627 Ok(())
628 } else {
629 Err(SketchError::InvalidHandle)
630 }
631 }
632
633 fn check_line(&self, id: LineId) -> Result<(), SketchError> {
634 if self.lines.contains(id) {
635 Ok(())
636 } else {
637 Err(SketchError::InvalidHandle)
638 }
639 }
640
641 fn check_circle(&self, id: CircleId) -> Result<(), SketchError> {
642 if self.circles.contains(id) {
643 Ok(())
644 } else {
645 Err(SketchError::InvalidHandle)
646 }
647 }
648
649 fn check_arc(&self, id: ArcId) -> Result<(), SketchError> {
650 if self.arcs.contains(id) {
651 Ok(())
652 } else {
653 Err(SketchError::InvalidHandle)
654 }
655 }
656}
657
658fn build_snapshot_from_params(
660 params: &[f64],
661 _param_map: &[ParamRef],
662 param_index: &HashMap<ParamRef, usize>,
663 sys: &GcsSystem,
664) -> EntitySnapshot {
665 let points = sys
666 .points
667 .iter()
668 .map(|(id, data)| {
669 let x = param_index
670 .get(&ParamRef::PointX(id))
671 .map_or(data.x, |&i| params[i]);
672 let y = param_index
673 .get(&ParamRef::PointY(id))
674 .map_or(data.y, |&i| params[i]);
675 (id, (x, y))
676 })
677 .collect();
678
679 let lines = sys.lines.iter().map(|(id, d)| (id, (d.p1, d.p2))).collect();
680
681 let circles = sys
682 .circles
683 .iter()
684 .map(|(id, data)| {
685 let r = param_index
686 .get(&ParamRef::CircleRadius(id))
687 .map_or(data.radius, |&i| params[i]);
688 (id, (data.center, r))
689 })
690 .collect();
691
692 let arcs = sys
693 .arcs
694 .iter()
695 .map(|(id, d)| (id, (d.center, d.start, d.end)))
696 .collect();
697
698 EntitySnapshot {
699 points,
700 lines,
701 circles,
702 arcs,
703 }
704}
705
706fn constraint_references_point(c: &Constraint, id: PointId) -> bool {
708 match c {
709 Constraint::Coincident(p1, p2) | Constraint::Distance(p1, p2, _) => *p1 == id || *p2 == id,
710 Constraint::PointLineDistance(pt, _, _)
711 | Constraint::PointOnCircle(pt, _)
712 | Constraint::PointOnArc(pt, _) => *pt == id,
713 Constraint::FixX(p, _) | Constraint::FixY(p, _) => *p == id,
714 Constraint::TangentLineArc(_, _, shared) | Constraint::TangentArcArc(_, _, shared) => {
715 *shared == id
716 }
717 Constraint::Horizontal(_)
718 | Constraint::Vertical(_)
719 | Constraint::Angle(_, _, _)
720 | Constraint::Perpendicular(_, _)
721 | Constraint::Parallel(_, _)
722 | Constraint::EqualRadiusArcArc(_, _)
723 | Constraint::EqualRadiusArcCircle(_, _)
724 | Constraint::ArcLength(_, _)
725 | Constraint::ConcentricArcArc(_, _)
726 | Constraint::ConcentricArcCircle(_, _) => false,
727 }
728}
729
730fn constraint_references_line(c: &Constraint, id: LineId) -> bool {
732 match c {
733 Constraint::Horizontal(l) | Constraint::Vertical(l) => *l == id,
734 Constraint::PointLineDistance(_, l, _) => *l == id,
735 Constraint::TangentLineArc(l, _, _) => *l == id,
736 Constraint::Angle(l1, l2, _)
737 | Constraint::Perpendicular(l1, l2)
738 | Constraint::Parallel(l1, l2) => *l1 == id || *l2 == id,
739 Constraint::Coincident(_, _)
740 | Constraint::Distance(_, _, _)
741 | Constraint::FixX(_, _)
742 | Constraint::FixY(_, _)
743 | Constraint::PointOnCircle(_, _)
744 | Constraint::PointOnArc(_, _)
745 | Constraint::TangentArcArc(_, _, _)
746 | Constraint::EqualRadiusArcArc(_, _)
747 | Constraint::EqualRadiusArcCircle(_, _)
748 | Constraint::ArcLength(_, _)
749 | Constraint::ConcentricArcArc(_, _)
750 | Constraint::ConcentricArcCircle(_, _) => false,
751 }
752}
753
754fn constraint_references_circle(c: &Constraint, id: CircleId) -> bool {
756 match c {
757 Constraint::PointOnCircle(_, circ) => *circ == id,
758 Constraint::EqualRadiusArcCircle(_, circ) | Constraint::ConcentricArcCircle(_, circ) => {
759 *circ == id
760 }
761 Constraint::Coincident(_, _)
762 | Constraint::Distance(_, _, _)
763 | Constraint::PointLineDistance(_, _, _)
764 | Constraint::FixX(_, _)
765 | Constraint::FixY(_, _)
766 | Constraint::Horizontal(_)
767 | Constraint::Vertical(_)
768 | Constraint::Angle(_, _, _)
769 | Constraint::Perpendicular(_, _)
770 | Constraint::Parallel(_, _)
771 | Constraint::PointOnArc(_, _)
772 | Constraint::TangentLineArc(_, _, _)
773 | Constraint::TangentArcArc(_, _, _)
774 | Constraint::EqualRadiusArcArc(_, _)
775 | Constraint::ArcLength(_, _)
776 | Constraint::ConcentricArcArc(_, _) => false,
777 }
778}
779
780fn constraint_references_arc(c: &Constraint, id: ArcId) -> bool {
782 match c {
783 Constraint::PointOnArc(_, arc) | Constraint::ArcLength(arc, _) => *arc == id,
784 Constraint::TangentLineArc(_, arc, _) => *arc == id,
785 Constraint::TangentArcArc(a1, a2, _)
786 | Constraint::EqualRadiusArcArc(a1, a2)
787 | Constraint::ConcentricArcArc(a1, a2) => *a1 == id || *a2 == id,
788 Constraint::EqualRadiusArcCircle(arc, _) | Constraint::ConcentricArcCircle(arc, _) => {
789 *arc == id
790 }
791 Constraint::Coincident(_, _)
792 | Constraint::Distance(_, _, _)
793 | Constraint::PointLineDistance(_, _, _)
794 | Constraint::FixX(_, _)
795 | Constraint::FixY(_, _)
796 | Constraint::Horizontal(_)
797 | Constraint::Vertical(_)
798 | Constraint::Angle(_, _, _)
799 | Constraint::Perpendicular(_, _)
800 | Constraint::Parallel(_, _)
801 | Constraint::PointOnCircle(_, _) => false,
802 }
803}
804
805#[cfg(test)]
806#[allow(clippy::unwrap_used, clippy::expect_used)]
807mod tests;