1use std::collections::{BTreeMap, BTreeSet};
12
13use geop_core_math::geop_error::{GeopError, GeopResult};
14use serde::{Deserialize, Serialize};
15
16macro_rules! define_ids {
17 ($($(#[$doc:meta])* $name:ident => $prefix:literal),* $(,)?) => {
18 $(
19 $(#[$doc])*
20 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
21 #[serde(transparent)]
22 pub struct $name(pub u64);
23
24 impl std::fmt::Display for $name {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 write!(f, concat!($prefix, "{}"), self.0)
28 }
29 }
30
31 impl<'de> Deserialize<'de> for $name {
32 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
33 d.deserialize_any(IdVisitor).map($name)
34 }
35 }
36 )*
37 };
38}
39
40struct IdVisitor;
46
47impl serde::de::Visitor<'_> for IdVisitor {
48 type Value = u64;
49
50 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
51 f.write_str("a non-negative integer id, as a number or a string")
52 }
53
54 fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<u64, E> {
55 Ok(v)
56 }
57
58 fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<u64, E> {
59 u64::try_from(v).map_err(|_| E::custom(format!("id {v} is negative")))
60 }
61
62 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<u64, E> {
63 v.parse()
64 .map_err(|_| E::custom(format!("{v:?} is not an integer id")))
65 }
66}
67
68define_ids!(
69 PointId => "p",
71 CurveId => "c",
73 ConstraintId => "k",
75);
76
77#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
78pub struct Point {
79 pub x: f64,
80 pub y: f64,
81}
82
83impl Point {
84 pub fn xy(&self) -> [f64; 2] {
85 [self.x, self.y]
86 }
87}
88
89#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
90#[serde(tag = "type")]
91pub enum CurveKind {
92 Line {
93 start: PointId,
94 end: PointId,
95 },
96 Arc {
101 start: PointId,
102 end: PointId,
103 sweep: f64,
104 },
105 Circle {
106 center: PointId,
107 radius: f64,
108 },
109 Spline {
113 control_points: Vec<PointId>,
114 },
115}
116
117#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
118pub struct Curve {
119 #[serde(flatten)]
120 pub kind: CurveKind,
121 #[serde(default)]
124 pub construction: bool,
125}
126
127impl Curve {
128 pub fn points(&self) -> Vec<PointId> {
130 match &self.kind {
131 CurveKind::Line { start, end } | CurveKind::Arc { start, end, .. } => {
132 vec![*start, *end]
133 }
134 CurveKind::Circle { center, .. } => vec![*center],
135 CurveKind::Spline { control_points } => control_points.clone(),
136 }
137 }
138
139 pub fn endpoints(&self) -> Option<(PointId, PointId)> {
141 match &self.kind {
142 CurveKind::Line { start, end } | CurveKind::Arc { start, end, .. } => {
143 Some((*start, *end))
144 }
145 CurveKind::Circle { .. } => None,
146 CurveKind::Spline { control_points } => {
147 Some((control_points[0], *control_points.last()?))
148 }
149 }
150 }
151}
152
153#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
156#[serde(tag = "type")]
157pub enum Constraint {
158 Coincident {
160 a: PointId,
161 b: PointId,
162 },
163 PointOnCurve {
166 point: PointId,
167 curve: CurveId,
168 },
169 Horizontal {
170 line: CurveId,
171 },
172 Vertical {
173 line: CurveId,
174 },
175 Parallel {
176 a: CurveId,
177 b: CurveId,
178 },
179 Perpendicular {
180 a: CurveId,
181 b: CurveId,
182 },
183 Collinear {
185 a: CurveId,
186 b: CurveId,
187 },
188 Tangent {
192 a: CurveId,
193 b: CurveId,
194 },
195 Equal {
197 a: CurveId,
198 b: CurveId,
199 },
200 Concentric {
202 a: CurveId,
203 b: CurveId,
204 },
205 Midpoint {
207 point: PointId,
208 curve: CurveId,
209 },
210 Symmetric {
212 a: PointId,
213 b: PointId,
214 line: CurveId,
215 },
216 Fix {
218 point: PointId,
219 x: f64,
220 y: f64,
221 },
222 Distance {
223 a: PointId,
224 b: PointId,
225 value: f64,
226 },
227 DistanceX {
229 a: PointId,
230 b: PointId,
231 value: f64,
232 },
233 DistanceY {
235 a: PointId,
236 b: PointId,
237 value: f64,
238 },
239 PointLineDistance {
240 point: PointId,
241 line: CurveId,
242 value: f64,
243 },
244 Length {
246 curve: CurveId,
247 value: f64,
248 },
249 Radius {
251 curve: CurveId,
252 value: f64,
253 },
254 Angle {
256 a: CurveId,
257 b: CurveId,
258 value: f64,
259 },
260}
261
262impl Constraint {
263 pub fn points(&self) -> Vec<PointId> {
265 use Constraint::*;
266 match *self {
267 Coincident { a, b }
268 | Distance { a, b, .. }
269 | DistanceX { a, b, .. }
270 | DistanceY { a, b, .. }
271 | Symmetric { a, b, .. } => vec![a, b],
272 PointOnCurve { point, .. }
273 | Midpoint { point, .. }
274 | Fix { point, .. }
275 | PointLineDistance { point, .. } => vec![point],
276 _ => Vec::new(),
277 }
278 }
279}
280
281pub type Positions = BTreeMap<PointId, [f64; 2]>;
285
286#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
296pub struct Sketch {
297 pub points: BTreeMap<PointId, Point>,
298 pub curves: BTreeMap<CurveId, Curve>,
299 pub constraints: BTreeMap<ConstraintId, Constraint>,
300 pub next_id: u64,
304}
305
306impl Sketch {
307 pub fn new() -> Self {
308 Self::default()
309 }
310
311 pub fn point(&self, id: PointId) -> GeopResult<&Point> {
312 self.points
313 .get(&id)
314 .ok_or_else(|| GeopError::new(format!("sketch has no point {id}")))
315 }
316
317 pub fn curve(&self, id: CurveId) -> GeopResult<&Curve> {
318 self.curves
319 .get(&id)
320 .ok_or_else(|| GeopError::new(format!("sketch has no curve {id}")))
321 }
322
323 pub fn positions(&self) -> Positions {
325 self.points.iter().map(|(&id, p)| (id, p.xy())).collect()
326 }
327
328 fn fresh_id(&mut self) -> u64 {
329 let id = self.next_id;
330 self.next_id += 1;
331 id
332 }
333
334 fn claim_id(&mut self, id: u64, taken: bool) -> GeopResult<()> {
339 if taken {
340 return Err(GeopError::new(format!("sketch id {id} is already in use")));
341 }
342 self.next_id = self.next_id.max(id + 1);
343 Ok(())
344 }
345
346 pub fn insert_point(&mut self, id: PointId, point: Point) -> GeopResult<()> {
350 self.claim_id(id.0, self.points.contains_key(&id))?;
351 self.points.insert(id, point);
352 Ok(())
353 }
354
355 pub fn insert_curve(&mut self, id: CurveId, curve: Curve) -> GeopResult<()> {
357 self.claim_id(id.0, self.curves.contains_key(&id))?;
358 self.curves.insert(id, curve);
359 Ok(())
360 }
361
362 pub fn add_point(&mut self, x: f64, y: f64) -> PointId {
363 let id = PointId(self.fresh_id());
364 self.points.insert(id, Point { x, y });
365 id
366 }
367
368 fn add_curve(&mut self, kind: CurveKind) -> CurveId {
369 let id = CurveId(self.fresh_id());
370 self.curves.insert(
371 id,
372 Curve {
373 kind,
374 construction: false,
375 },
376 );
377 id
378 }
379
380 pub fn add_line(&mut self, start: PointId, end: PointId) -> CurveId {
381 self.add_curve(CurveKind::Line { start, end })
382 }
383
384 pub fn add_arc(&mut self, start: PointId, end: PointId, curvature: f64) -> CurveId {
389 let [sx, sy] = self.points[&start].xy();
390 let [ex, ey] = self.points[&end].xy();
391 let chord = (ex - sx).hypot(ey - sy);
392 let sweep = 2.0 * (curvature * chord / 2.0).clamp(-1.0, 1.0).asin();
393 self.add_arc_with_sweep(start, end, sweep)
394 }
395
396 pub fn add_arc_with_sweep(&mut self, start: PointId, end: PointId, sweep: f64) -> CurveId {
397 self.add_curve(CurveKind::Arc { start, end, sweep })
398 }
399
400 pub fn add_circle(&mut self, center: PointId, radius: f64) -> CurveId {
401 self.add_curve(CurveKind::Circle { center, radius })
402 }
403
404 pub fn add_spline(&mut self, control_points: Vec<PointId>) -> CurveId {
405 self.add_curve(CurveKind::Spline { control_points })
406 }
407
408 pub fn set_construction(&mut self, curve: CurveId, construction: bool) {
409 if let Some(c) = self.curves.get_mut(&curve) {
410 c.construction = construction;
411 }
412 }
413
414 pub fn constrain(&mut self, constraint: Constraint) -> ConstraintId {
415 let id = ConstraintId(self.fresh_id());
416 self.constraints.insert(id, constraint);
417 id
418 }
419
420 pub fn validate(&self) -> GeopResult<()> {
423 let max_id = [
424 self.points.keys().last().map(|id| id.0),
425 self.curves.keys().last().map(|id| id.0),
426 self.constraints.keys().last().map(|id| id.0),
427 ];
428 if let Some(max_id) = max_id.into_iter().flatten().max()
429 && max_id >= self.next_id
430 {
431 return Err(GeopError::new(format!(
432 "sketch uses id {max_id}, but its next_id is only {}: new entities would reuse ids",
433 self.next_id
434 )));
435 }
436 for (&i, curve) in &self.curves {
437 for p in curve.points() {
438 self.point(p)
439 .map_err(|e| e.with_context(format!("curve {i}")))?;
440 }
441 match &curve.kind {
442 CurveKind::Line { start, end } | CurveKind::Arc { start, end, .. }
443 if start == end =>
444 {
445 return Err(GeopError::new(format!(
446 "curve {i} starts and ends at the same point"
447 )));
448 }
449 CurveKind::Arc { sweep, .. }
450 if sweep.is_nan() || sweep.abs() >= std::f64::consts::TAU =>
451 {
452 return Err(GeopError::new(format!(
453 "arc {i} has sweep {sweep}, which is not within (-2π, 2π)"
454 )));
455 }
456 CurveKind::Spline { control_points } if control_points.len() < 2 => {
457 return Err(GeopError::new(format!(
458 "spline {i} needs at least 2 control points"
459 )));
460 }
461 _ => {}
462 }
463 }
464 for (&i, c) in &self.constraints {
465 self.validate_constraint(c)
466 .map_err(|e| e.with_context(format!("constraint {i} = {c:?}")))?;
467 }
468 Ok(())
469 }
470
471 fn validate_constraint(&self, c: &Constraint) -> GeopResult<()> {
472 use Constraint::*;
473 for p in c.points() {
474 self.point(p)?;
475 }
476 let kind = |id: CurveId| self.curve(id).map(|c| &c.kind);
477 let is_line = |id| Ok::<_, GeopError>(matches!(kind(id)?, CurveKind::Line { .. }));
478 let is_round = |id| {
479 Ok::<_, GeopError>(matches!(
480 kind(id)?,
481 CurveKind::Arc { .. } | CurveKind::Circle { .. }
482 ))
483 };
484 let need = |ok: bool, what: &str| {
485 if ok {
486 Ok(())
487 } else {
488 Err(GeopError::new(format!("operands must be {what}")))
489 }
490 };
491 match c {
492 Coincident { .. }
493 | Distance { .. }
494 | DistanceX { .. }
495 | DistanceY { .. }
496 | Fix { .. } => Ok(()),
497 PointOnCurve { curve, .. } => need(
498 !matches!(kind(*curve)?, CurveKind::Spline { .. }),
499 "a point and a line, arc or circle",
500 ),
501 Horizontal { line } | Vertical { line } => need(is_line(*line)?, "a line"),
502 Parallel { a, b }
503 | Perpendicular { a, b }
504 | Collinear { a, b }
505 | Angle { a, b, .. } => need(is_line(*a)? && is_line(*b)?, "two lines"),
506 Tangent { a, b } => {
507 let shared = self.shared_endpoint(*a, *b)?.is_some();
508 let ok = shared
509 && !(is_line(*a)? && is_line(*b)?)
510 && !matches!(kind(*a)?, CurveKind::Circle { .. })
511 && !matches!(kind(*b)?, CurveKind::Circle { .. })
512 || !shared
513 && (is_round(*a)? && (is_round(*b)? || is_line(*b)?)
514 || is_line(*a)? && is_round(*b)?);
515 need(
516 ok,
517 "curves sharing an endpoint (not two lines), a line and a circle/arc, or two circles/arcs",
518 )
519 }
520 Equal { a, b } => need(
521 is_line(*a)? && is_line(*b)? || is_round(*a)? && is_round(*b)?,
522 "two lines or two circles/arcs",
523 ),
524 Concentric { a, b } => need(is_round(*a)? && is_round(*b)?, "two circles/arcs"),
525 Midpoint { curve, .. } => need(
526 matches!(
527 kind(*curve)?,
528 CurveKind::Line { .. } | CurveKind::Arc { .. }
529 ),
530 "a point and a line or arc",
531 ),
532 Symmetric { line, .. } => need(is_line(*line)?, "two points and a line"),
533 PointLineDistance { line, .. } => need(is_line(*line)?, "a point and a line"),
534 Length { curve, .. } => need(
535 matches!(
536 kind(*curve)?,
537 CurveKind::Line { .. } | CurveKind::Arc { .. }
538 ),
539 "a line or arc",
540 ),
541 Radius { curve, .. } => need(is_round(*curve)?, "a circle or arc"),
542 }
543 }
544
545 pub fn point_classes(&self) -> BTreeMap<PointId, PointId> {
549 fn find(parent: &mut BTreeMap<PointId, PointId>, i: PointId) -> PointId {
550 let mut r = i;
551 while parent[&r] != r {
552 r = parent[&r];
553 }
554 let mut i = i;
555 while parent[&i] != r {
556 let next = parent[&i];
557 parent.insert(i, r);
558 i = next;
559 }
560 r
561 }
562 let mut parent: BTreeMap<PointId, PointId> =
563 self.points.keys().map(|&id| (id, id)).collect();
564 for c in self.constraints.values() {
565 if let Constraint::Coincident { a, b } = *c {
566 let (ra, rb) = (find(&mut parent, a), find(&mut parent, b));
567 let (lo, hi) = (ra.min(rb), ra.max(rb));
570 parent.insert(hi, lo);
571 }
572 }
573 self.points
574 .keys()
575 .map(|&id| (id, find(&mut parent, id)))
576 .collect()
577 }
578
579 pub fn on_line(&self, line: CurveId) -> GeopResult<BTreeSet<PointId>> {
588 let CurveKind::Line { start, end } = self.curve(line)?.kind else {
589 return Err(GeopError::new(format!("curve {line} is not a line")));
590 };
591 let class = self.point_classes();
592 let mut on = BTreeSet::new();
593 let mut mark = |p: PointId| {
594 on.insert(class[&p]);
595 };
596 mark(start);
597 mark(end);
598 for c in self.constraints.values() {
599 match *c {
600 Constraint::PointOnCurve { point, curve }
601 | Constraint::Midpoint { point, curve }
602 if curve == line =>
603 {
604 mark(point)
605 }
606 Constraint::Collinear { a, b } if a == line || b == line => {
607 let other = if a == line { b } else { a };
608 for p in self.curve(other)?.points() {
609 mark(p);
610 }
611 }
612 _ => {}
613 }
614 }
615 Ok(class
616 .iter()
617 .filter(|(_, rep)| on.contains(rep))
618 .map(|(&p, _)| p)
619 .collect())
620 }
621
622 pub fn shared_endpoint(&self, a: CurveId, b: CurveId) -> GeopResult<Option<(bool, bool)>> {
625 let (Some((a0, a1)), Some((b0, b1))) =
626 (self.curve(a)?.endpoints(), self.curve(b)?.endpoints())
627 else {
628 return Ok(None);
629 };
630 let class = self.point_classes();
631 let same = |p: PointId, q: PointId| class[&p] == class[&q];
632 Ok([(false, false), (false, true), (true, false), (true, true)]
633 .into_iter()
634 .find(|&(ea, eb)| same(if ea { a1 } else { a0 }, if eb { b1 } else { b0 })))
635 }
636}