1mod curves;
42mod geometry;
43mod solve;
44#[cfg(test)]
45mod tests;
46mod wire;
47
48pub use curves::{Chain3d, Piece3d};
49pub use geometry::{Arc3, Helix3};
50pub use solve::Solve3dReport;
51pub use wire::WireEdge;
52
53use std::collections::BTreeMap;
54
55use geop_core_geometry::nurb_curve::NurbCurve3D;
56use geop_core_math::{
57 geop_error::{GeopError, GeopResult},
58 scalars::{Scalar, as_f64},
59 vector::Vector3,
60};
61use serde::{Deserialize, Serialize};
62
63use crate::{
64 ConstraintId, CurveId, PointId,
65 sketch::{Uses, is_false, point_classes, remove},
66};
67
68#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
70pub struct Point3d {
71 pub at: Vector3,
72 #[serde(default, skip_serializing_if = "is_false")]
74 pub fixed: bool,
75}
76
77#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
79#[serde(tag = "type")]
80pub enum CurveKind3d {
81 Line {
82 start: PointId,
83 end: PointId,
84 },
85 Arc {
87 start: PointId,
88 through: PointId,
89 end: PointId,
90 },
91 Spline {
93 points: Vec<PointId>,
94 },
95 Helix {
101 start: PointId,
102 base: PointId,
103 top: PointId,
104 end: PointId,
105 #[serde(with = "as_f64")]
106 turns: Scalar,
107 #[serde(default, skip_serializing_if = "is_false")]
108 left: bool,
109 },
110 Reference,
113}
114
115#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
116pub struct Curve3d {
117 #[serde(flatten)]
118 pub kind: CurveKind3d,
119 #[serde(default, skip_serializing_if = "is_false")]
121 pub construction: bool,
122}
123
124impl Uses for Curve3d {
125 fn points(&self) -> Vec<PointId> {
126 Curve3d::points(self)
127 }
128}
129
130impl Curve3d {
131 pub fn points(&self) -> Vec<PointId> {
133 match &self.kind {
134 CurveKind3d::Line { start, end } => vec![*start, *end],
135 CurveKind3d::Arc {
136 start,
137 through,
138 end,
139 } => vec![*start, *through, *end],
140 CurveKind3d::Spline { points } => points.clone(),
141 CurveKind3d::Helix {
142 start,
143 base,
144 top,
145 end,
146 ..
147 } => vec![*start, *base, *top, *end],
148 CurveKind3d::Reference => Vec::new(),
149 }
150 }
151
152 pub fn endpoints(&self) -> Option<(PointId, PointId)> {
155 match &self.kind {
156 CurveKind3d::Line { start, end }
157 | CurveKind3d::Arc { start, end, .. }
158 | CurveKind3d::Helix { start, end, .. } => Some((*start, *end)),
159 CurveKind3d::Spline { points } => Some((*points.first()?, *points.last()?)),
160 CurveKind3d::Reference => None,
161 }
162 }
163
164 pub fn is_drawn(&self) -> bool {
167 !matches!(self.kind, CurveKind3d::Reference)
168 }
169}
170
171#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
173#[serde(rename_all = "snake_case")]
174pub enum End {
175 Start,
176 End,
177}
178
179#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
181#[serde(rename_all = "snake_case")]
182pub enum Coordinate {
183 X,
184 Y,
185 Z,
186}
187
188impl Coordinate {
189 pub const ALL: [Coordinate; 3] = [Coordinate::X, Coordinate::Y, Coordinate::Z];
190
191 pub fn index(self) -> usize {
193 match self {
194 Coordinate::X => 0,
195 Coordinate::Y => 1,
196 Coordinate::Z => 2,
197 }
198 }
199
200 pub fn name(self) -> &'static str {
201 match self {
202 Coordinate::X => "x",
203 Coordinate::Y => "y",
204 Coordinate::Z => "z",
205 }
206 }
207}
208
209#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
211#[serde(tag = "type")]
212pub enum Constraint3d {
213 Coincident { a: PointId, b: PointId },
215 Coordinate {
217 point: PointId,
218 axis: Coordinate,
219 #[serde(with = "as_f64")]
220 value: Scalar,
221 },
222 Distance {
223 a: PointId,
224 b: PointId,
225 #[serde(with = "as_f64")]
226 value: Scalar,
227 },
228 Length {
230 line: CurveId,
231 #[serde(with = "as_f64")]
232 value: Scalar,
233 },
234 Radius {
236 arc: CurveId,
237 #[serde(with = "as_f64")]
238 value: Scalar,
239 },
240 Parallel { a: CurveId, b: CurveId },
242 ParallelTo { line: CurveId, direction: Vector3 },
244 TangentTo {
247 curve: CurveId,
248 end: End,
249 direction: Vector3,
250 },
251 OnCurve { point: PointId, curve: CurveId },
254 Tangent { a: CurveId, b: CurveId },
256 HelixEnd { helix: CurveId },
258}
259
260impl Uses for Constraint3d {
261 fn points(&self) -> Vec<PointId> {
262 Constraint3d::points(self)
263 }
264
265 fn curves(&self) -> Vec<CurveId> {
266 Constraint3d::curves(self)
267 }
268
269 fn coincident(&self) -> Option<(PointId, PointId)> {
270 match *self {
271 Constraint3d::Coincident { a, b } => Some((a, b)),
272 _ => None,
273 }
274 }
275}
276
277impl Constraint3d {
278 pub fn points(&self) -> Vec<PointId> {
280 use Constraint3d::*;
281 match *self {
282 Coincident { a, b } | Distance { a, b, .. } => vec![a, b],
283 Coordinate { point, .. } | OnCurve { point, .. } => vec![point],
284 _ => Vec::new(),
285 }
286 }
287
288 pub fn curves(&self) -> Vec<CurveId> {
290 use Constraint3d::*;
291 match *self {
292 Length { line: c, .. }
293 | Radius { arc: c, .. }
294 | ParallelTo { line: c, .. }
295 | TangentTo { curve: c, .. }
296 | OnCurve { curve: c, .. }
297 | HelixEnd { helix: c } => vec![c],
298 Parallel { a, b } | Tangent { a, b } => vec![a, b],
299 Coincident { .. } | Coordinate { .. } | Distance { .. } => Vec::new(),
300 }
301 }
302
303 pub fn value(&self) -> Option<Scalar> {
305 use Constraint3d::*;
306 match *self {
307 Coordinate { value, .. }
308 | Distance { value, .. }
309 | Length { value, .. }
310 | Radius { value, .. } => Some(value),
311 _ => None,
312 }
313 }
314
315 pub fn set_value(&mut self, v: Scalar) {
317 use Constraint3d::*;
318 match self {
319 Coordinate { value, .. }
320 | Distance { value, .. }
321 | Length { value, .. }
322 | Radius { value, .. } => *value = v,
323 _ => {}
324 }
325 }
326}
327
328#[derive(Clone, Debug, PartialEq)]
331pub struct Enclosure3d {
332 pub points: BTreeMap<PointId, Vector3>,
333}
334
335impl Enclosure3d {
336 pub fn as_drawn(sketch: &Sketch3d) -> Self {
338 Enclosure3d {
339 points: sketch.points.iter().map(|(&id, p)| (id, p.at)).collect(),
340 }
341 }
342}
343
344#[derive(Clone, Debug, Default, Serialize, Deserialize)]
347pub struct Sketch3d {
348 pub points: BTreeMap<PointId, Point3d>,
349 pub curves: BTreeMap<CurveId, Curve3d>,
350 pub constraints: BTreeMap<ConstraintId, Constraint3d>,
351 pub next_id: u64,
353 #[serde(skip)]
357 pub references: BTreeMap<CurveId, NurbCurve3D>,
358}
359
360impl PartialEq for Sketch3d {
363 fn eq(&self, other: &Self) -> bool {
364 self.points == other.points
365 && self.curves == other.curves
366 && self.constraints == other.constraints
367 && self.next_id == other.next_id
368 }
369}
370
371impl Sketch3d {
372 pub fn new() -> Self {
373 Self::default()
374 }
375
376 pub fn point(&self, id: PointId) -> GeopResult<&Point3d> {
377 self.points
378 .get(&id)
379 .ok_or_else(|| GeopError::new(format!("3-D sketch has no point {id}")))
380 }
381
382 pub fn curve(&self, id: CurveId) -> GeopResult<&Curve3d> {
383 self.curves
384 .get(&id)
385 .ok_or_else(|| GeopError::new(format!("3-D sketch has no curve {id}")))
386 }
387
388 fn fresh_id(&mut self) -> u64 {
389 let id = self.next_id;
390 self.next_id += 1;
391 id
392 }
393
394 pub fn add_point(&mut self, at: Vector3) -> PointId {
395 let id = PointId(self.fresh_id());
396 self.points.insert(id, Point3d { at, fixed: false });
397 id
398 }
399
400 pub fn add_fixed_point(&mut self, at: Vector3) -> PointId {
402 let id = PointId(self.fresh_id());
403 self.points.insert(id, Point3d { at, fixed: true });
404 id
405 }
406
407 pub fn add_curve(&mut self, kind: CurveKind3d) -> CurveId {
408 let id = CurveId(self.fresh_id());
409 self.curves.insert(
410 id,
411 Curve3d {
412 kind,
413 construction: false,
414 },
415 );
416 id
417 }
418
419 pub fn add_line(&mut self, start: PointId, end: PointId) -> CurveId {
420 self.add_curve(CurveKind3d::Line { start, end })
421 }
422
423 pub fn add_arc(&mut self, start: PointId, through: PointId, end: PointId) -> CurveId {
424 self.add_curve(CurveKind3d::Arc {
425 start,
426 through,
427 end,
428 })
429 }
430
431 pub fn add_helix(
434 &mut self,
435 start: PointId,
436 base: PointId,
437 top: PointId,
438 turns: Scalar,
439 left: bool,
440 ) -> GeopResult<CurveId> {
441 let at = |p: PointId| self.point(p).map(|p| p.at);
442 let end = (Helix3 {
443 start: at(start)?,
444 base: at(base)?,
445 top: at(top)?,
446 turns,
447 left,
448 })
449 .end()?;
450 let end = self.add_point(end);
451 let helix = self.add_curve(CurveKind3d::Helix {
452 start,
453 base,
454 top,
455 end,
456 turns,
457 left,
458 });
459 self.constrain(Constraint3d::HelixEnd { helix });
460 Ok(helix)
461 }
462
463 pub fn add_spline(&mut self, points: Vec<PointId>) -> CurveId {
464 self.add_curve(CurveKind3d::Spline { points })
465 }
466
467 pub fn add_reference(&mut self, curve: NurbCurve3D) -> CurveId {
469 let id = self.add_curve(CurveKind3d::Reference);
470 self.references.insert(id, curve);
471 id
472 }
473
474 pub fn constrain(&mut self, constraint: Constraint3d) -> ConstraintId {
475 let id = ConstraintId(self.fresh_id());
476 self.constraints.insert(id, constraint);
477 id
478 }
479
480 pub fn remove(&mut self, points: &[PointId], curves: &[CurveId], constraints: &[ConstraintId]) {
484 let (all_points, all_curves) = (&mut self.points, &mut self.curves);
485 let dead_curves = remove(
486 all_points,
487 all_curves,
488 &mut self.constraints,
489 points,
490 curves,
491 constraints,
492 );
493 self.references.retain(|id, _| !dead_curves.contains(id));
494 }
495
496 pub fn point_classes(&self) -> BTreeMap<PointId, PointId> {
500 point_classes(&self.points, &self.constraints)
501 }
502
503 pub fn shared_end(&self, a: CurveId, b: CurveId) -> GeopResult<Option<(End, End)>> {
505 let (Some((a0, a1)), Some((b0, b1))) =
506 (self.curve(a)?.endpoints(), self.curve(b)?.endpoints())
507 else {
508 return Ok(None);
509 };
510 let class = self.point_classes();
511 let at = |p: PointId| class[&p];
512 let pick = |e: End, s: PointId, t: PointId| if e == End::Start { s } else { t };
513 Ok([
514 (End::Start, End::Start),
515 (End::Start, End::End),
516 (End::End, End::Start),
517 (End::End, End::End),
518 ]
519 .into_iter()
520 .find(|&(ea, eb)| at(pick(ea, a0, a1)) == at(pick(eb, b0, b1))))
521 }
522
523 pub fn validate(&self) -> GeopResult<()> {
526 let max_id = [
527 self.points.keys().last().map(|id| id.0),
528 self.curves.keys().last().map(|id| id.0),
529 self.constraints.keys().last().map(|id| id.0),
530 ];
531 if let Some(max_id) = max_id.into_iter().flatten().max()
532 && max_id >= self.next_id
533 {
534 return Err(GeopError::new(format!(
535 "3-D sketch uses id {max_id}, but its next_id is only {}",
536 self.next_id
537 )));
538 }
539 let class = self.point_classes();
540 for (&i, curve) in &self.curves {
541 for p in curve.points() {
542 self.point(p)
543 .map_err(|e| e.with_context(format!("curve {i}")))?;
544 }
545 let distinct = |ps: &[PointId]| ps.windows(2).all(|w| class[&w[0]] != class[&w[1]]);
546 match &curve.kind {
547 CurveKind3d::Line { start, end } if class[start] == class[end] => {
548 return Err(GeopError::new(format!(
549 "line {i} starts and ends at the same point"
550 )));
551 }
552 CurveKind3d::Arc {
553 start,
554 through,
555 end,
556 } if !distinct(&[*start, *through, *end]) || class[start] == class[end] => {
557 return Err(GeopError::new(format!(
558 "arc {i} needs three different points"
559 )));
560 }
561 CurveKind3d::Spline { points } if points.len() < 2 || !distinct(points) => {
562 return Err(GeopError::new(format!(
563 "spline {i} needs at least two points, each different from the one before"
564 )));
565 }
566 CurveKind3d::Helix {
567 start,
568 base,
569 top,
570 turns,
571 ..
572 } if class[base] == class[top]
573 || class[start] == class[base]
574 || !turns.definitely_greater(Scalar::ZERO) =>
575 {
576 return Err(GeopError::new(format!(
577 "helix {i} needs an axis, a start off it and more than no turns"
578 )));
579 }
580 CurveKind3d::Reference if !self.references.contains_key(&i) => {
581 return Err(GeopError::new(format!(
582 "reference curve {i} has no geometry handed in"
583 )));
584 }
585 _ => {}
586 }
587 }
588 let mut directed: BTreeMap<(CurveId, End), ConstraintId> = BTreeMap::new();
590 for (&k, c) in &self.constraints {
591 self.validate_constraint(c)
592 .map_err(|e| e.with_context(format!("constraint {k} = {c:?}")))?;
593 for (curve, end) in self.spline_ends_directed(c)? {
594 if let Some(other) = directed.insert((curve, end), k) {
595 return Err(GeopError::new(format!(
596 "the {end:?} of spline {curve} is given its direction twice, by constraints {other} and {k}: a spline end leaves along one direction"
597 )));
598 }
599 }
600 }
601 Ok(())
602 }
603
604 fn validate_constraint(&self, c: &Constraint3d) -> GeopResult<()> {
605 use Constraint3d::*;
606 for p in c.points() {
607 self.point(p)?;
608 }
609 let kind = |id: CurveId| self.curve(id).map(|c| &c.kind);
610 let is_line = |id| Ok::<_, GeopError>(matches!(kind(id)?, CurveKind3d::Line { .. }));
611 let need = |ok: bool, what: &str| {
612 if ok {
613 Ok(())
614 } else {
615 Err(GeopError::new(format!("operands must be {what}")))
616 }
617 };
618 match c {
619 Coincident { .. } | Coordinate { .. } | Distance { .. } => Ok(()),
620 Length { line, .. } => need(is_line(*line)?, "a line"),
621 Radius { arc, .. } => need(matches!(kind(*arc)?, CurveKind3d::Arc { .. }), "an arc"),
622 Parallel { a, b } => need(is_line(*a)? && is_line(*b)?, "two lines"),
623 ParallelTo { line, direction } => {
624 need(is_line(*line)?, "a line")?;
625 direction.normalize().map(|_| ())
626 }
627 TangentTo {
628 curve, direction, ..
629 } => {
630 need(
631 matches!(
632 kind(*curve)?,
633 CurveKind3d::Arc { .. } | CurveKind3d::Spline { .. }
634 ),
635 "an arc or a spline",
636 )?;
637 direction.normalize().map(|_| ())
638 }
639 OnCurve { curve, .. } => need(
640 !matches!(
641 kind(*curve)?,
642 CurveKind3d::Spline { .. } | CurveKind3d::Helix { .. }
643 ),
644 "a point and a line, an arc or an edge (a point on a spline or a helix is not supported)",
645 ),
646 Tangent { a, b } => {
647 need(
648 self.curve(*a)?.is_drawn() && self.curve(*b)?.is_drawn(),
649 "two drawn curves",
650 )?;
651 let helix = |c: CurveId| matches!(kind(c), Ok(CurveKind3d::Helix { .. }));
652 need(!helix(*a) && !helix(*b), "curves other than a helix")?;
653 need(
654 self.shared_end(*a, *b)?.is_some(),
655 "two curves meeting at an end of each",
656 )
657 }
658 HelixEnd { helix } => need(
659 matches!(kind(*helix)?, CurveKind3d::Helix { .. }),
660 "a helix",
661 ),
662 }
663 }
664
665 fn spline_ends_directed(&self, c: &Constraint3d) -> GeopResult<Vec<(CurveId, End)>> {
669 Ok(match *c {
670 Constraint3d::TangentTo { curve, end, .. }
671 if matches!(self.curve(curve)?.kind, CurveKind3d::Spline { .. }) =>
672 {
673 vec![(curve, end)]
674 }
675 Constraint3d::Tangent { a, b } => self.adopts(a, b)?.into_iter().collect(),
676 _ => Vec::new(),
677 })
678 }
679
680 pub fn adopts(&self, a: CurveId, b: CurveId) -> GeopResult<Option<(CurveId, End)>> {
686 let Some((ea, eb)) = self.shared_end(a, b)? else {
687 return Ok(None);
688 };
689 let spline = |c: CurveId| -> GeopResult<bool> {
690 Ok(matches!(self.curve(c)?.kind, CurveKind3d::Spline { .. }))
691 };
692 Ok(match (spline(a)?, spline(b)?) {
693 (false, false) => None,
694 (true, false) => Some((a, ea)),
695 (false, true) => Some((b, eb)),
696 (true, true) if a > b => Some((a, ea)),
697 (true, true) => Some((b, eb)),
698 })
699 }
700}