1use crate::sketch::{CurveId, CurveKind, Enclosure, PointId, Sketch, SplineShape};
22use crate::text;
23use geop_core_geometry::{
24 aabb::union_aabb,
25 intersection::curve_curve_intersect,
26 nurb_curve::{NurbCurve, NurbCurve2D, loop_signed_area},
27};
28use geop_core_math::{
29 geop_error::{GeopError, GeopResult, WithContext},
30 scalars::Scalar,
31 solvers::{
32 probe::{Probe, attempts},
33 repeat,
34 walk::walk_by,
35 },
36 vector::Vector2,
37 with_context,
38};
39use std::collections::{BTreeMap, BTreeSet};
40
41const MAX_CROSSINGS: usize = 16;
46const MAX_NODES: usize = 4000;
47const MIN_SUBDIVISION: i64 = 1_000_000;
48const MAX_RAY_ATTEMPTS: usize = 32;
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub struct ProfileEdge {
55 pub curve: CurveId,
56 pub reversed: bool,
57 pub contour: usize,
60}
61
62#[derive(Clone, Debug, PartialEq, Eq)]
64pub struct ProfileLoop {
65 pub edges: Vec<ProfileEdge>,
66}
67
68#[derive(Clone, Debug, PartialEq, Eq)]
70pub struct Chain {
71 pub edges: Vec<ProfileEdge>,
72 pub closed: bool,
74}
75
76#[derive(Clone, Debug, PartialEq, Eq)]
79pub struct Region {
80 pub outer: ProfileLoop,
81 pub holes: Vec<ProfileLoop>,
82}
83
84#[derive(Clone, Debug, PartialEq, Eq)]
88pub enum Shape {
89 Regions(Vec<Region>),
90 Chain(ProfileLoop),
91}
92
93const SAMPLES: usize = 16;
95
96impl Sketch {
97 pub fn shape(&self) -> GeopResult<Shape> {
100 self.validate()?;
101 if self.loops()?.is_empty() {
102 Ok(Shape::Chain(self.sweep_chain()?))
103 } else {
104 Ok(Shape::Regions(self.regions()?))
105 }
106 }
107
108 fn sweep_chain(&self) -> GeopResult<ProfileLoop> {
111 let curves: Vec<CurveId> = self
112 .curves
113 .iter()
114 .filter(|(_, c)| !c.construction && c.endpoints().is_some())
115 .map(|(&id, _)| id)
116 .collect();
117 if curves.is_empty() {
118 return Err(GeopError::new("sketch has no curves to sweep"));
119 }
120 let ctx = with_context!(
121 "one sketch is one profile to sweep: draw each chain in a sketch of its own"
122 );
123 let chain = self.chain(&curves).with_context(ctx)?;
124 Ok(ProfileLoop { edges: chain.edges })
125 }
126
127 pub fn chain(&self, curves: &[CurveId]) -> GeopResult<Chain> {
132 let mut ids: Vec<CurveId> = Vec::new();
133 for &c in curves {
134 self.curve(c)?;
135 if !ids.contains(&c) {
136 ids.push(c);
137 }
138 }
139 let Some(&first) = ids.first() else {
140 return Err(GeopError::new("no curves to make a chain of"));
141 };
142 if let Some(&circle) = ids
143 .iter()
144 .find(|c| matches!(self.curves[c].kind, CurveKind::Circle { .. }))
145 {
146 if ids.len() > 1 {
147 return Err(GeopError::new(format!(
148 "circle {circle} is a chain of its own, apart from the other curves"
149 )));
150 }
151 return Ok(Chain {
152 edges: vec![ProfileEdge {
153 curve: circle,
154 reversed: false,
155 contour: 0,
156 }],
157 closed: true,
158 });
159 }
160 let class = self.point_classes();
161 let ends = |c: CurveId| {
162 let (s, e) = self.curves[&c].endpoints().expect("no circle");
163 (class[&s], class[&e])
164 };
165 let mut at: BTreeMap<PointId, Vec<CurveId>> = BTreeMap::new();
166 for &c in &ids {
167 let (s, e) = ends(c);
168 at.entry(s).or_default().push(c);
169 at.entry(e).or_default().push(c);
170 }
171 if let Some((p, cs)) = at.iter().find(|(_, cs)| cs.len() > 2) {
172 return Err(GeopError::new(format!(
173 "curves branch at point {p}: {} curves meet there",
174 cs.len()
175 )));
176 }
177 let start = at.iter().find(|(_, cs)| cs.len() == 1).map(|(&p, cs)| {
179 let c = cs[0];
180 (c, ends(c).0 != p)
181 });
182 let closed = start.is_none();
183 let (c, reversed) = start.unwrap_or((first, false));
184 let edges = walk_by(
187 ProfileEdge {
188 curve: c,
189 reversed,
190 contour: 0,
191 },
192 |edge| edge.curve,
193 |edge| {
194 let (s, e) = ends(edge.curve);
195 let end = if edge.reversed { s } else { e };
196 Ok::<_, GeopError>(at[&end].iter().find(|&&k| k != edge.curve).map(|&next| {
197 ProfileEdge {
198 curve: next,
199 reversed: ends(next).1 == end,
200 contour: 0,
201 }
202 }))
203 },
204 )?
205 .nodes;
206 if let Some(apart) = ids
207 .iter()
208 .find(|&&k| !edges.iter().any(|edge| edge.curve == k))
209 {
210 return Err(GeopError::new(format!(
211 "the curves form separate chains: {apart} is not joined to {}",
212 edges[0].curve
213 )));
214 }
215 Ok(Chain { edges, closed })
216 }
217
218 pub fn runs(&self) -> GeopResult<Vec<Chain>> {
225 self.validate()?;
226 let class = self.point_classes();
227 let lone = |curve, contour| Chain {
228 edges: vec![ProfileEdge {
229 curve,
230 reversed: false,
231 contour,
232 }],
233 closed: true,
234 };
235 let mut runs = Vec::new();
236 let mut open: BTreeMap<CurveId, (PointId, PointId)> = BTreeMap::new();
239 let mut at: BTreeMap<PointId, Vec<CurveId>> = BTreeMap::new();
240 for (&id, curve) in self.curves.iter().filter(|(_, c)| !c.construction) {
241 match (curve.endpoints(), &curve.kind) {
242 (None, CurveKind::Text { text, .. }) => {
243 runs.extend((0..text::outlines(text)?.len()).map(|k| lone(id, k)));
244 }
245 (Some((s, e)), _) if class[&s] != class[&e] => {
246 open.insert(id, (class[&s], class[&e]));
247 at.entry(class[&s]).or_default().push(id);
248 at.entry(class[&e]).or_default().push(id);
249 }
250 (Some(_), kind) if !matches!(kind, CurveKind::Spline { .. }) => {
251 return Err(GeopError::new(format!(
252 "curve {id} starts and ends at the same point"
253 )));
254 }
255 _ => runs.push(lone(id, 0)),
256 }
257 }
258 let mut left: BTreeSet<CurveId> = open.keys().copied().collect();
261 let mut walk = |start: PointId, first: CurveId| {
262 let (mut point, mut next, mut edges) = (start, Some(first), Vec::new());
263 while let Some(curve) = next.filter(|c| left.remove(c)) {
264 let (s, e) = open[&curve];
265 edges.push(ProfileEdge {
266 curve,
267 reversed: s != point,
268 contour: 0,
269 });
270 point = if s == point { e } else { s };
271 next = match &at[&point][..] {
272 [a, b] => Some(if *a == curve { *b } else { *a }),
273 _ => None,
274 };
275 }
276 Chain {
277 closed: !edges.is_empty() && point == start,
278 edges,
279 }
280 };
281 for (&point, curves) in at.iter().filter(|(_, curves)| curves.len() != 2) {
283 for &curve in curves {
284 runs.push(walk(point, curve));
285 }
286 }
287 for &curve in open.keys() {
288 runs.push(walk(open[&curve].0, curve));
289 }
290 runs.retain(|run| !run.edges.is_empty());
291 Ok(runs)
292 }
293
294 pub fn chain_through(&self, curve: CurveId) -> GeopResult<Vec<CurveId>> {
298 let construction = self.curve(curve)?.construction;
299 let Some((s, e)) = self.curves[&curve].endpoints() else {
300 return Ok(vec![curve]);
301 };
302 let class = self.point_classes();
303 let kin: Vec<(CurveId, PointId, PointId)> = self
304 .curves
305 .iter()
306 .filter(|(_, c)| c.construction == construction)
307 .filter_map(|(&id, c)| c.endpoints().map(|(s, e)| (id, class[&s], class[&e])))
308 .collect();
309 let others_at = |p: PointId, not: CurveId| -> Vec<(CurveId, PointId)> {
310 kin.iter()
311 .filter(|&&(id, s, e)| id != not && (s == p || e == p))
312 .map(|&(id, s, e)| (id, if s == p { e } else { s }))
313 .collect()
314 };
315 let mut chain = vec![curve];
316 for from in [class[&e], class[&s]] {
317 let on = walk_by(
320 (curve, from),
321 |&(c, _)| c,
322 |&(c, p)| {
323 Ok::<_, GeopError>(match others_at(p, c)[..] {
324 [(next, far)] => Some((next, far)),
325 _ => None,
326 })
327 },
328 )?;
329 let new: Vec<CurveId> = (on.nodes[1..].iter())
330 .map(|&(c, _)| c)
331 .take_while(|c| !chain.contains(c))
332 .collect();
333 chain.extend(new);
334 }
335 Ok(chain)
336 }
337
338 pub fn regions(&self) -> GeopResult<Vec<Region>> {
340 self.validate()?;
341 let loops = self.loops()?;
342 if loops.is_empty() {
343 return Err(GeopError::new(
344 "sketch has no closed profile: its curves do not form a loop",
345 ));
346 }
347 let drawn = Enclosure::as_drawn(self);
349 let curves: Vec<Vec<NurbCurve2D>> = loops
350 .iter()
351 .map(|l| {
352 Ok(l.to_nurbs(self, &drawn)?
353 .into_iter()
354 .map(|p| p.curve)
355 .collect())
356 })
357 .collect::<GeopResult<_>>()?;
358 let extent = extent_of(&curves)?;
359 let counter_clockwise: Vec<bool> = (loops.iter().zip(&curves))
360 .map(|(l, c)| winds_counter_clockwise(l, c))
361 .collect::<GeopResult<_>>()?;
362 let bounds: Vec<Option<[Scalar; 3]>> = (curves.iter())
363 .map(|c| union_aabb(c.iter().map(NurbCurve::aabb)))
364 .collect();
365
366 let mut containers: Vec<Vec<usize>> = Vec::with_capacity(loops.len());
371 for i in 0..loops.len() {
372 let probe = midpoint(&curves[i][0])?;
373 let mut inside = Vec::new();
374 for (j, other) in curves.iter().enumerate() {
375 let boxed =
376 bounds[j].is_some_and(|b| (0..2).all(|k| b[k].could_be_equal(probe[k])));
377 if j != i && boxed && loop_contains(other, &probe, extent)? {
378 inside.push(j);
379 }
380 }
381 containers.push(inside);
382 }
383
384 let mut regions: Vec<(usize, Region)> = Vec::new();
385 for i in (0..loops.len()).filter(|&i| containers[i].len().is_multiple_of(2)) {
386 let outer = if counter_clockwise[i] {
387 loops[i].clone()
388 } else {
389 loops[i].reversed()
390 };
391 regions.push((
392 i,
393 Region {
394 outer,
395 holes: Vec::new(),
396 },
397 ));
398 }
399 for i in (0..loops.len()).filter(|&i| !containers[i].len().is_multiple_of(2)) {
400 let depth = containers[i].len();
401 let parent = *containers[i]
402 .iter()
403 .find(|&&j| containers[j].len() == depth - 1)
404 .expect("an odd-depth loop lies directly inside an even-depth one");
405 let hole = if counter_clockwise[i] {
406 loops[i].reversed()
407 } else {
408 loops[i].clone()
409 };
410 regions
411 .iter_mut()
412 .find(|(j, _)| *j == parent)
413 .unwrap()
414 .1
415 .holes
416 .push(hole);
417 }
418 Ok(regions.into_iter().map(|(_, r)| r).collect())
419 }
420
421 fn loops(&self) -> GeopResult<Vec<ProfileLoop>> {
423 let class = self.point_classes();
424 let mut loops = Vec::new();
425 let mut edges: Vec<(CurveId, PointId, PointId)> = Vec::new();
428 for (&id, curve) in &self.curves {
429 if curve.construction {
430 continue;
431 }
432 match curve.endpoints() {
433 Some((s, e)) if class[&s] != class[&e] => edges.push((id, class[&s], class[&e])),
434 Some(_) if !matches!(curve.kind, CurveKind::Spline { .. }) => {
435 return Err(GeopError::new(format!(
436 "curve {id} starts and ends at the same point"
437 )));
438 }
439 None if let CurveKind::Text { text, .. } = &curve.kind => {
441 let contours = text::outlines(text)?.len();
442 loops.extend((0..contours).map(|contour| ProfileLoop {
443 edges: vec![ProfileEdge {
444 curve: id,
445 reversed: false,
446 contour,
447 }],
448 }));
449 }
450 _ => loops.push(ProfileLoop {
452 edges: vec![ProfileEdge {
453 curve: id,
454 reversed: false,
455 contour: 0,
456 }],
457 }),
458 }
459 }
460
461 let mut alive = vec![true; edges.len()];
463 let mut degree: BTreeMap<PointId, usize> = BTreeMap::new();
464 for &(_, a, b) in &edges {
465 *degree.entry(a).or_default() += 1;
466 *degree.entry(b).or_default() += 1;
467 }
468 let _ = repeat::resolve((0..edges.len()).collect(), |&k| {
469 let (_, a, b) = edges[k];
470 if degree[&a] != 1 && degree[&b] != 1 {
471 return Err(());
472 }
473 alive[k] = false;
474 *degree.get_mut(&a).unwrap() -= 1;
475 *degree.get_mut(&b).unwrap() -= 1;
476 Ok(())
477 });
478 if let Some((p, d)) = degree.iter().find(|(_, d)| **d > 2) {
479 return Err(GeopError::new(format!(
480 "profile curves branch at point {p}: {d} curves meet there"
481 )));
482 }
483
484 let mut used = vec![false; edges.len()];
486 for start in 0..edges.len() {
487 if !alive[start] || used[start] {
488 continue;
489 }
490 let around = walk_by(
492 (start, false),
493 |&(k, _)| k,
494 |&(k, at_end)| {
495 used[k] = true;
496 let (_, a, b) = edges[k];
497 let next_point = if at_end { a } else { b };
498 Ok::<_, GeopError>(
499 (0..edges.len())
500 .find(|&j| {
501 alive[j]
502 && !used[j]
503 && (edges[j].1 == next_point || edges[j].2 == next_point)
504 })
505 .map(|next| (next, edges[next].2 == next_point)),
506 )
507 },
508 )?;
509 let lp = (around.nodes.iter())
510 .map(|&(k, at_end)| ProfileEdge {
511 curve: edges[k].0,
512 reversed: at_end,
513 contour: 0,
514 })
515 .collect();
516 loops.push(ProfileLoop { edges: lp });
517 }
518 Ok(loops)
519 }
520}
521
522#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
533pub enum ProfileJoint {
534 Point(PointId),
535 Split { curve: CurveId, index: usize },
536}
537
538impl std::fmt::Display for ProfileJoint {
540 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
541 match self {
542 ProfileJoint::Point(p) => write!(f, "{p}"),
543 ProfileJoint::Split { curve, index } => write!(f, "{curve}@{index}"),
544 }
545 }
546}
547
548#[derive(Clone, Debug)]
554pub struct Piece<C> {
555 pub curve: C,
556 pub source: CurveId,
557 pub index: usize,
558 pub start: ProfileJoint,
559 pub end: ProfileJoint,
560}
561
562pub type ProfilePiece = Piece<NurbCurve2D>;
564
565impl<C> Piece<C> {
566 pub fn name(&self) -> String {
569 if self.index == 0 {
570 format!("{}", self.source)
571 } else {
572 format!("{}#{}", self.source, self.index)
573 }
574 }
575}
576
577impl ProfileLoop {
578 pub fn reversed(&self) -> ProfileLoop {
580 ProfileLoop {
581 edges: self
582 .edges
583 .iter()
584 .rev()
585 .map(|e| ProfileEdge {
586 reversed: !e.reversed,
587 ..*e
588 })
589 .collect(),
590 }
591 }
592
593 pub fn to_nurbs(&self, sketch: &Sketch, geometry: &Enclosure) -> GeopResult<Vec<ProfilePiece>> {
609 let mut out = Vec::new();
610 for edge in &self.edges {
611 let curve = edge.curve;
612 let ctx = with_context!("converting sketch curve {curve} to NURBS");
613 let (pieces, offset, (first, last)) = match &sketch.curve(curve)?.kind {
617 CurveKind::Text { .. } => {
618 let contours = text_contours(sketch, geometry, curve).with_context(ctx)?;
619 let offset = contours.iter().take(edge.contour).map(Vec::len).sum();
620 let pieces = (contours.into_iter().nth(edge.contour)).ok_or_else(|| {
621 GeopError::new(format!("text {curve} has no contour {}", edge.contour))
622 })?;
623 let seam = ProfileJoint::Split {
624 curve,
625 index: offset,
626 };
627 (pieces, offset, (seam, seam))
628 }
629 _ => {
630 let pieces = curve_nurbs(sketch, geometry, curve).with_context(ctx)?;
631 (pieces, 0, curve_joints(sketch, curve).with_context(ctx)?)
632 }
633 };
634 let n = pieces.len();
635 let joint = |j: usize| match j {
638 0 => first,
639 j if j == n => last,
640 j => ProfileJoint::Split {
641 curve,
642 index: offset + j,
643 },
644 };
645 let pieces = pieces.into_iter().enumerate().map(|(j, c)| ProfilePiece {
646 curve: c,
647 source: curve,
648 index: offset + j,
649 start: joint(j),
650 end: joint(j + 1),
651 });
652 if edge.reversed {
653 out.extend(pieces.rev().map(|p| ProfilePiece {
654 curve: p.curve.reverse(),
655 start: p.end,
656 end: p.start,
657 ..p
658 }));
659 } else {
660 out.extend(pieces);
661 }
662 }
663 if let [only] = &out[..]
664 && only.start == only.end
665 {
666 let (a, b) = only.curve.split(Scalar::ONE.div(Scalar::TWO)?)?;
669 let middle = ProfileJoint::Split {
670 curve: only.source,
671 index: 1,
672 };
673 let (first, second) = if self.edges[0].reversed {
676 (1, 0)
677 } else {
678 (0, 1)
679 };
680 out = vec![
681 ProfilePiece {
682 curve: a.with_unit_domain()?,
683 source: only.source,
684 index: first,
685 start: only.start,
686 end: middle,
687 },
688 ProfilePiece {
689 curve: b.with_unit_domain()?,
690 source: only.source,
691 index: second,
692 start: middle,
693 end: only.end,
694 },
695 ];
696 }
697 Ok(out)
698 }
699
700 pub fn polyline(&self, sketch: &Sketch) -> GeopResult<Vec<Vector2>> {
702 let mut out = Vec::new();
703 for edge in &self.edges {
704 let mut pts = match &sketch.curve(edge.curve)?.kind {
705 CurveKind::Text { .. } => {
706 let mut contours = curve_polylines(sketch, edge.curve)?;
707 if edge.contour >= contours.len() {
708 return Err(GeopError::new(format!(
709 "text {} has no contour {}",
710 edge.curve, edge.contour
711 )));
712 }
713 contours.swap_remove(edge.contour)
714 }
715 _ => curve_polyline(sketch, edge.curve)?,
716 };
717 if edge.reversed {
718 pts.reverse();
719 }
720 pts.pop();
722 out.extend(pts);
723 }
724 Ok(out)
725 }
726}
727
728fn curve_joints(sketch: &Sketch, curve: CurveId) -> GeopResult<(ProfileJoint, ProfileJoint)> {
731 Ok(match sketch.curve(curve)?.endpoints() {
732 Some((s, e)) => (ProfileJoint::Point(s), ProfileJoint::Point(e)),
733 None => {
734 let seam = ProfileJoint::Split { curve, index: 0 };
735 (seam, seam)
736 }
737 })
738}
739
740pub fn curve_polyline(sketch: &Sketch, curve: CurveId) -> GeopResult<Vec<Vector2>> {
745 if let CurveKind::Text { .. } = sketch.curve(curve)?.kind {
746 return Err(GeopError::new(format!(
747 "text {curve} is drawn contour by contour, not as one line"
748 )));
749 }
750 let pieces = curve_nurbs(sketch, &Enclosure::as_drawn(sketch), curve)?;
751 polyline(&pieces, NurbCurve2D::evaluate)
752}
753
754pub fn curve_polylines(sketch: &Sketch, curve: CurveId) -> GeopResult<Vec<Vec<Vector2>>> {
757 match sketch.curve(curve)?.kind {
758 CurveKind::Text { .. } => (text_contours(sketch, &Enclosure::as_drawn(sketch), curve)?)
759 .iter()
760 .map(|contour| polyline(contour, NurbCurve2D::evaluate))
761 .collect(),
762 _ => Ok(vec![curve_polyline(sketch, curve)?]),
763 }
764}
765
766pub fn text_contours(
769 sketch: &Sketch,
770 geometry: &Enclosure,
771 curve: CurveId,
772) -> GeopResult<Vec<Vec<NurbCurve2D>>> {
773 let CurveKind::Text {
774 anchor,
775 text,
776 height,
777 angle,
778 } = &sketch.curve(curve)?.kind
779 else {
780 return Err(GeopError::new(format!("curve {curve} is no text")));
781 };
782 text::place(
783 &text::outlines(text)?,
784 geometry.points[anchor],
785 *height,
786 *angle,
787 )
788}
789
790pub(crate) fn polyline<const D: usize, P>(
795 pieces: &[NurbCurve<D>],
796 evaluate: impl Fn(&NurbCurve<D>, Scalar) -> GeopResult<P>,
797) -> GeopResult<Vec<P>> {
798 let mut out = Vec::new();
799 for piece in pieces {
800 let spans = if piece.degree == 1 {
801 1
802 } else {
803 SAMPLES * (piece.control_points.len() - 1)
804 };
805 out.pop();
807 for i in 0..=spans {
808 out.push(evaluate(
809 piece,
810 Scalar::from_ratio(i as i64, spans as i64)?,
811 )?);
812 }
813 }
814 Ok(out)
815}
816
817fn spline_degree(n: usize) -> usize {
819 3.min(n - 1)
820}
821
822pub(crate) fn spline_knots(n: usize, degree: usize) -> GeopResult<Vec<Scalar>> {
824 let spans = n - degree;
825 let mut knots = vec![Scalar::ZERO; degree + 1];
826 for i in 1..spans {
827 knots.push(Scalar::from_ratio(i as i64, spans as i64)?);
828 }
829 knots.extend(std::iter::repeat_n(Scalar::ONE, degree + 1));
830 Ok(knots)
831}
832
833pub fn curve_nurbs(
839 sketch: &Sketch,
840 geometry: &Enclosure,
841 curve: CurveId,
842) -> GeopResult<Vec<NurbCurve2D>> {
843 let at = |p: &PointId| geometry.points[p];
844 let param = || geometry.params[&curve];
845 let one_half = Scalar::ONE.div(Scalar::TWO)?;
846 match &sketch.curve(curve)?.kind {
847 CurveKind::Line { start, end } => Ok(vec![NurbCurve2D::line(at(start), at(end))?]),
848 CurveKind::Arc { start, end, sweep } => {
849 let (s, e) = (at(start), at(end));
850 if sweep.is_sharp() && sweep.could_be_equal(Scalar::ZERO) {
851 return Ok(vec![NurbCurve2D::line(s, e)?]);
852 }
853 let quarter = Scalar::PI.div(Scalar::TWO)?;
858 let pieces = (1..=4)
859 .find(|&k| {
860 !sweep
861 .abs()
862 .definitely_greater(quarter.mul(Scalar::from_i64(k as i64)))
863 })
864 .unwrap_or(4);
865 let delta = param().div(Scalar::from_i64(pieces as i64))?;
866 let half = delta.div(Scalar::TWO)?;
867 let mut ends = vec![s];
872 if pieces > 1 {
873 let whole = param().div(Scalar::TWO)?;
874 let chord = e.sub(&s);
875 let length = chord.norm();
876 let left = chord.normalize()?.perp();
877 let mid = s.add(&e).prod_scalar(one_half);
878 let offset = length.mul(one_half).mul(whole.cos()).div(whole.sin())?;
879 let center = mid.add(&left.prod_scalar(offset));
880 ends.extend((1..pieces).map(|j| {
881 let angle = delta.mul(Scalar::from_i64(j as i64));
882 center.add(&s.sub(¢er).rotate(angle.cos(), angle.sin()))
883 }));
884 }
885 ends.push(e);
886 ends.windows(2)
887 .map(|w| {
888 let chord = w[1].sub(&w[0]);
893 let length = chord.norm();
894 let left = chord.normalize()?.perp();
895 let bulge = length.mul(one_half).mul(half.sin()).div(half.cos())?;
896 let mid = w[0].add(&w[1]).prod_scalar(one_half);
897 let m = mid.sub(&left.prod_scalar(bulge));
898 NurbCurve2D::arc(w[0], m, w[1], half.cos())
899 })
900 .collect()
901 }
902 CurveKind::Circle { center, .. } => {
903 let c = at(center);
904 let r = param();
905 let point = |x: Scalar, y: Scalar| c.add(&Vector2::from_array([x, y]));
906 let q = [
907 point(r, Scalar::ZERO),
908 point(Scalar::ZERO, r),
909 point(r.neg(), Scalar::ZERO),
910 point(Scalar::ZERO, r.neg()),
911 ];
912 let corners = [
913 point(r, r),
914 point(r.neg(), r),
915 point(r.neg(), r.neg()),
916 point(r, r.neg()),
917 ];
918 let w = one_half.sqrt()?;
920 (0..4)
921 .map(|j| NurbCurve2D::arc(q[j], corners[j], q[(j + 1) % 4], w))
922 .collect()
923 }
924 CurveKind::Spline {
925 control_points,
926 shape,
927 } => {
928 let points: Vec<Vector2> = control_points.iter().map(at).collect();
929 Ok(vec![spline_nurbs(&points, shape.as_ref())?])
930 }
931 CurveKind::Text { .. } => Ok(text_contours(sketch, geometry, curve)?
932 .into_iter()
933 .flatten()
934 .collect()),
935 }
936}
937
938pub(crate) fn basis_at(
946 n: usize,
947 shape: Option<&SplineShape>,
948 t: Scalar,
949 derivative: bool,
950) -> GeopResult<Vec<(usize, Scalar, Scalar)>> {
951 let one_hot = |j: usize| -> Vec<Vector2> {
952 (0..n)
953 .map(|k| {
954 let one = if k == j { Scalar::ONE } else { Scalar::ZERO };
955 Vector2::from_array([one, Scalar::ZERO])
956 })
957 .collect()
958 };
959 let whole = spline_nurbs(&one_hot(0), shape)?;
960 let (degree, knots) = (whole.degree, &whole.knot_vector);
961 let mut out = Vec::new();
962 for j in 0..n {
963 if t.definitely_less(knots[j]) || t.definitely_greater(knots[j + degree + 1]) {
964 continue;
965 }
966 let spline = spline_nurbs(&one_hot(j), shape)?;
967 let value = spline.evaluate(t)?[0];
968 let slope = if derivative {
969 spline.tangent(t)?[0]
970 } else {
971 Scalar::ZERO
972 };
973 out.push((j, value, slope));
974 }
975 Ok(out)
976}
977
978pub(crate) fn spline_nurbs(
981 points: &[Vector2],
982 shape: Option<&SplineShape>,
983) -> GeopResult<NurbCurve2D> {
984 let n = points.len();
985 let Some(shape) = shape else {
986 let degree = spline_degree(n);
987 return NurbCurve::try_new(
988 degree,
989 points.iter().map(|p| p.homogeneous(Scalar::ONE)).collect(),
990 spline_knots(n, degree)?,
991 );
992 };
993 let curve = NurbCurve::try_new(
996 shape.degree,
997 points
998 .iter()
999 .zip(&shape.weights)
1000 .map(|(p, &w)| p.homogeneous(w))
1001 .collect(),
1002 shape.knots.clone(),
1003 )?;
1004 curve.with_unit_domain()
1005}
1006
1007fn extent_of(loops: &[Vec<NurbCurve2D>]) -> GeopResult<Scalar> {
1013 let bounds = union_aabb(loops.iter().flatten().map(NurbCurve::aabb));
1014 let span = |k: usize| bounds.map_or(Scalar::ZERO, |b| b[k].upper().sub(b[k].lower()));
1015 let extent = span(0).mul(span(0)).add(span(1).mul(span(1))).sqrt()?;
1016 if !extent.definitely_greater(Scalar::ZERO) {
1017 return Err(GeopError::new("the profile's loops have no extent"));
1018 }
1019 Ok(extent)
1020}
1021
1022fn midpoint(curve: &NurbCurve2D) -> GeopResult<Vector2> {
1024 Ok(curve.evaluate(curve.mid_parameter()?)?.sharpen())
1025}
1026
1027fn ray(from: &Vector2, dir: &Vector2, extent: Scalar) -> GeopResult<NurbCurve2D> {
1030 let to = from
1031 .add(&dir.prod_scalar(Scalar::from_i64(3).mul(extent)))
1032 .sharpen();
1033 NurbCurve2D::line(*from, to)
1034}
1035
1036fn loop_contains(curves: &[NurbCurve2D], probe: &Vector2, extent: Scalar) -> GeopResult<bool> {
1048 let min_subdivision = Scalar::from_ratio(1, MIN_SUBDIVISION)?;
1049 let golden = Scalar::PI.mul(Scalar::from_i64(3).sub(Scalar::from_i64(5).sqrt()?));
1051 let mut last_rejection = String::new();
1052 let found = attempts(MAX_RAY_ATTEMPTS, |&k| -> GeopResult<_> {
1054 let angle = golden.mul(Scalar::from_i64(k as i64));
1055 let dir = Vector2::from_array([angle.cos(), angle.sin()]).sharpen();
1056 let ray = ray(probe, &dir, extent)?;
1057 let mut crossings = 0usize;
1058 for curve in curves {
1059 let hits =
1060 match curve_curve_intersect(&ray, curve, MAX_CROSSINGS, MAX_NODES, min_subdivision)
1061 {
1062 Ok(hits) if !hits.is_coincident() => hits.into_vec(),
1063 Ok(_) => {
1064 last_rejection = format!("the ray runs along {curve:?}");
1065 return Ok(Probe::Next);
1066 }
1067 Err(e) => {
1068 last_rejection =
1069 format!("the ray against {curve:?} did not converge: {e:?}");
1070 return Ok(Probe::Next);
1071 }
1072 };
1073 let (t0, t1) = curve.domain();
1074 for (along_ray, along_curve) in hits {
1075 if !along_ray.definitely_greater(Scalar::ZERO) {
1076 continue;
1080 }
1081 let from_start = along_curve.sub(t0).abs();
1082 let from_end = along_curve.sub(t1).abs();
1083 if !from_start.definitely_greater(min_subdivision)
1084 || !from_end.definitely_greater(min_subdivision)
1085 {
1086 last_rejection =
1087 format!("the ray grazes an end of {curve:?} at {along_curve:?}");
1088 return Ok(Probe::Next);
1089 }
1090 crossings += 1;
1091 }
1092 }
1093 Ok(Probe::Found(!crossings.is_multiple_of(2)))
1094 })?;
1095 if let Some(inside) = found {
1096 return Ok(inside);
1097 }
1098 Err(GeopError::new(format!(
1099 "could not classify {probe:?} against a loop: every ray direction was ambiguous, \
1100 the last because {last_rejection}"
1101 )))
1102}
1103
1104fn winds_counter_clockwise(lp: &ProfileLoop, curves: &[NurbCurve2D]) -> GeopResult<bool> {
1108 let area = loop_signed_area(curves)?;
1109 if area.definitely_greater(Scalar::ZERO) {
1110 Ok(true)
1111 } else if area.definitely_less(Scalar::ZERO) {
1112 Ok(false)
1113 } else {
1114 Err(GeopError::new(format!(
1115 "could not tell which way the loop {lp:?} winds: its area {area:?} could be zero"
1116 )))
1117 }
1118}