1use geop_core_geometry::{
13 nurb_curve::{NurbCurve, NurbCurve3D},
14 nurb_surface::{NurbSurface, NurbSurface3D},
15 shape::{Arc, Axis, Circle},
16};
17use geop_core_math::{
18 geop_error::{GeopError, GeopResult},
19 plain::{P3, add, cross, distance, dot, norm, normalize, scale, sub},
20 primitives::Pose,
21 scalars::Scalar,
22 vector::{Vector3, Vector4},
23};
24
25use super::reader::{Args, Reader, unsupported};
26
27#[derive(Clone, Copy, Debug, PartialEq)]
29pub struct Frame {
30 pub origin: P3,
31 pub x: P3,
32 pub y: P3,
33 pub z: P3,
34}
35
36impl Frame {
37 pub fn new(origin: P3, axis: P3, reference: P3) -> GeopResult<Self> {
41 let z = normalize(axis)
42 .ok_or_else(|| GeopError::new(format!("a placement's axis {axis:?} has no length")))?;
43 let x = normalize(sub(reference, scale(z, dot(reference, z))))
44 .or_else(|| {
45 let other = if z[0].abs() < 0.9 {
46 [1.0, 0.0, 0.0]
47 } else {
48 [0.0, 1.0, 0.0]
49 };
50 normalize(sub(other, scale(z, dot(other, z))))
51 })
52 .expect("a vector not along a unit axis has a perpendicular part");
53 Ok(Self {
54 origin,
55 x,
56 y: cross(z, x),
57 z,
58 })
59 }
60
61 pub fn point(&self, p: P3) -> P3 {
63 add(
64 self.origin,
65 add(
66 scale(self.x, p[0]),
67 add(scale(self.y, p[1]), scale(self.z, p[2])),
68 ),
69 )
70 }
71
72 pub fn local(&self, p: P3) -> P3 {
74 let d = sub(p, self.origin);
75 [dot(d, self.x), dot(d, self.y), dot(d, self.z)]
76 }
77}
78
79#[derive(Clone, Copy, Debug, PartialEq)]
81pub struct Placement {
82 pub axes: [P3; 3],
84 pub origin: P3,
85}
86
87impl Placement {
88 pub const IDENTITY: Placement = Placement {
89 axes: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
90 origin: [0.0; 3],
91 };
92
93 pub fn of(frame: &Frame) -> Self {
95 Placement {
96 axes: [frame.x, frame.y, frame.z],
97 origin: frame.origin,
98 }
99 }
100
101 pub fn direction(&self, d: P3) -> P3 {
102 add(
103 scale(self.axes[0], d[0]),
104 add(scale(self.axes[1], d[1]), scale(self.axes[2], d[2])),
105 )
106 }
107
108 pub fn point(&self, p: P3) -> P3 {
109 add(self.origin, self.direction(p))
110 }
111
112 pub fn then(&self, outer: &Placement) -> Placement {
114 Placement {
115 axes: self.axes.map(|a| outer.direction(a)),
116 origin: outer.point(self.origin),
117 }
118 }
119
120 pub fn inverse(&self) -> Placement {
121 let [a, b, c] = self.axes;
123 let axes = [[a[0], b[0], c[0]], [a[1], b[1], c[1]], [a[2], b[2], c[2]]];
124 let inverse = Placement {
125 axes,
126 origin: [0.0; 3],
127 };
128 Placement {
129 axes,
130 origin: scale(inverse.direction(self.origin), -1.0),
131 }
132 }
133}
134
135#[derive(Clone, Copy, Debug)]
138pub struct Scope {
139 pub length: f64,
141 pub angle: f64,
143 pub uncertainty: f64,
146 pub place: Placement,
147}
148
149#[derive(Clone, Debug)]
151pub enum CurveDef {
152 Line {
153 origin: P3,
154 direction: P3,
155 },
156 Circle {
157 frame: Frame,
158 radius: f64,
159 },
160 Ellipse {
161 frame: Frame,
162 a: f64,
163 b: f64,
164 },
165 Nurbs(NurbsCurve),
166 Polyline(Vec<P3>),
167 Conic {
169 frame: Frame,
170 kind: ConicKind,
171 },
172}
173
174#[derive(Clone, Copy, Debug)]
178pub enum Shape {
179 Circle {
180 center: P3,
181 normal: P3,
182 radius: f64,
183 },
184 Line {
185 origin: P3,
186 direction: P3,
187 },
188 Iso {
192 origin: P3,
193 axis: P3,
194 },
195}
196
197impl CurveDef {
198 pub fn shape(&self) -> Option<Shape> {
200 match self {
201 CurveDef::Circle { frame, radius } => Some(Shape::Circle {
202 center: frame.origin,
203 normal: frame.z,
204 radius: *radius,
205 }),
206 CurveDef::Line { origin, direction } => Some(Shape::Line {
207 origin: *origin,
208 direction: normalize(*direction)?,
209 }),
210 _ => None,
211 }
212 }
213}
214
215#[derive(Clone, Copy, Debug)]
216pub enum ConicKind {
217 Hyperbola { a: f64, b: f64 },
219 Parabola { focal: f64 },
221}
222
223#[derive(Clone, Debug)]
224pub struct NurbsCurve {
225 pub degree: usize,
226 pub points: Vec<P3>,
227 pub weights: Option<Vec<f64>>,
228 pub knots: Vec<f64>,
229}
230
231#[derive(Clone, Debug)]
234pub struct SurfaceDef {
235 pub kind: SurfaceKind,
236 pub flipped: bool,
240}
241
242#[derive(Clone, Debug)]
243pub enum SurfaceKind {
244 Plane(Frame),
245 Revolved(Revolved),
246 Nurbs(NurbsSurface),
247 Extrusion {
249 curve: NurbsCurve,
250 vector: P3,
251 },
252}
253
254#[derive(Clone, Debug)]
255pub struct NurbsSurface {
256 pub degree_u: usize,
257 pub degree_v: usize,
258 pub num_u: usize,
259 pub num_v: usize,
260 pub points: Vec<P3>,
262 pub weights: Option<Vec<f64>>,
263 pub knots_u: Vec<f64>,
264 pub knots_v: Vec<f64>,
265}
266
267#[derive(Clone, Debug)]
270pub struct Revolved {
271 pub frame: Frame,
272 pub profile: Profile,
273 pub on_axis: f64,
276}
277
278#[derive(Clone, Debug)]
282pub enum Profile {
283 Line {
286 rho: f64,
287 h: f64,
288 drho: f64,
289 dh: f64,
290 },
291 Sphere { radius: f64 },
293 Circle { rho: f64, h: f64, radius: f64 },
300 Curve(NurbsCurve),
302}
303
304impl Revolved {
305 pub fn chart(&self, p: P3) -> (Option<f64>, f64) {
309 let [x, y, h] = self.frame.local(p);
310 let rho = x.hypot(y);
311 let angle = (rho > self.on_axis).then(|| y.atan2(x));
312 let v = match &self.profile {
313 Profile::Line {
314 rho: r0,
315 h: h0,
316 drho,
317 dh,
318 } => {
319 let along =
324 |rho: f64| ((rho - r0) * drho + (h - h0) * dh) / (drho * drho + dh * dh);
325 let off = |rho: f64| ((rho - r0) * dh - (h - h0) * drho).abs();
326 if *drho != 0.0 && off(-rho) < off(rho) {
327 let angle = angle.map(|a| {
328 if a > 0.0 {
329 a - std::f64::consts::PI
330 } else {
331 a + std::f64::consts::PI
332 }
333 });
334 return (angle, along(-rho));
335 }
336 along(rho)
337 }
338 Profile::Curve(_) => h,
339 Profile::Sphere { .. } => h.atan2(rho),
340 Profile::Circle { rho: rc, h: hc, .. } => (h - hc).atan2(rho - rc),
341 };
342 (angle, v)
343 }
344
345 pub fn iso_line(&self, shape: &Shape, uncertainty: f64) -> bool {
352 let (o, z) = (self.frame.origin, self.frame.z);
353 match *shape {
354 Shape::Iso { origin, axis } => origin == o && axis == z,
355 Shape::Circle {
356 center,
357 normal,
358 radius,
359 } => {
360 norm(cross(normal, z)) * radius <= uncertainty
363 && norm(cross(sub(center, o), z)) <= uncertainty
364 }
365 Shape::Line { origin, direction } => {
366 let across = cross(direction, z);
370 dot(sub(origin, o), across).abs() <= uncertainty * norm(across)
371 }
372 }
373 }
374
375 pub fn past_axis(&self, p: P3) -> Option<f64> {
379 let Profile::Circle {
380 rho: rc,
381 h: hc,
382 radius,
383 } = self.profile
384 else {
385 return None;
386 };
387 if rc > radius {
388 return None;
389 }
390 let [x, y, h] = self.frame.local(p);
391 let rho = x.hypot(y);
392 let off = |rho: f64| ((rho - rc).hypot(h - hc) - radius).abs();
393 Some(off(rho) - off(-rho))
394 }
395
396 pub fn mirrored(&self) -> Revolved {
401 let profile = match self.profile {
402 Profile::Circle { rho, h, radius } => Profile::Circle {
403 rho: -rho,
404 h,
405 radius,
406 },
407 ref other => other.clone(),
408 };
409 Revolved {
410 profile,
411 ..self.clone()
412 }
413 }
414
415 pub fn v_is_angle(&self) -> bool {
418 matches!(self.profile, Profile::Circle { rho, radius, .. } if rho > radius)
419 }
420
421 pub fn poles(&self) -> Vec<f64> {
424 match &self.profile {
425 Profile::Circle { rho, radius, .. } if rho <= radius => {
429 let v = (-rho / radius).acos();
430 vec![-v, v]
431 }
432 Profile::Circle { .. } | Profile::Curve(_) => Vec::new(),
433 Profile::Line { drho, .. } if *drho == 0.0 => Vec::new(),
434 Profile::Line { rho, drho, .. } => vec![-rho / drho],
435 Profile::Sphere { .. } => {
436 vec![-std::f64::consts::FRAC_PI_2, std::f64::consts::FRAC_PI_2]
437 }
438 }
439 }
440
441 pub fn profile_point(&self, v: f64) -> P3 {
443 let (rho, h) = match &self.profile {
444 Profile::Line { rho, h, drho, dh } => (rho + v * drho, h + v * dh),
445 Profile::Sphere { radius } => (radius * v.cos(), radius * v.sin()),
446 Profile::Circle { rho, h, radius } => (rho + radius * v.cos(), h + radius * v.sin()),
447 Profile::Curve(_) => unreachable!("a curve profile has no analytic point"),
448 };
449 self.frame.point([rho, 0.0, h])
450 }
451
452 pub fn profile_curve(&self, v0: f64, v1: f64) -> GeopResult<NurbCurve3D> {
455 let s3 = |p: P3| Vector3::from_array(p.map(Scalar::from_f64));
456 match &self.profile {
457 Profile::Line { .. } => {
458 NurbCurve3D::line(s3(self.profile_point(v0)), s3(self.profile_point(v1)))
459 }
460 Profile::Sphere { radius } | Profile::Circle { radius, .. } => {
461 let center = match &self.profile {
462 Profile::Circle { rho, h, .. } => self.frame.point([*rho, 0.0, *h]),
463 _ => self.frame.origin,
464 };
465 Arc {
467 circle: Circle {
468 center: s3(center),
469 normal: s3(scale(self.frame.y, -1.0)),
470 radius: Scalar::from_f64(*radius),
471 },
472 start: s3(self.profile_point(v0)),
473 end: s3(self.profile_point(v1)),
474 }
475 .to_curve()
476 }
477 Profile::Curve(curve) => curve.to_nurbs(),
478 }
479 }
480
481 pub fn parallel(&self, v: f64, from: f64, to: f64) -> GeopResult<NurbCurve3D> {
486 let s3 = |p: P3| Vector3::from_array(p.map(Scalar::from_f64));
487 let p = self.profile_point(v);
488 let [rho, _, h] = self.frame.local(p);
489 let at = |angle: f64| -> GeopResult<Vector3> { Ok(self.turn(angle)?.apply(&s3(p))) };
490 let start = at(from)?;
491 let end = if to == from { start } else { at(to)? };
492 Arc {
493 circle: Circle {
494 center: s3(self.frame.point([0.0, 0.0, h])),
495 normal: s3(self.frame.z),
496 radius: Scalar::from_f64(rho),
497 },
498 start,
499 end,
500 }
501 .to_curve()
502 }
503
504 pub fn axis(&self) -> GeopResult<Axis> {
505 Axis::try_new(
506 Vector3::from_array(self.frame.origin.map(Scalar::from_f64)),
507 Vector3::from_array(self.frame.z.map(Scalar::from_f64)),
508 )
509 }
510
511 pub fn patch(&self, from: f64, to: f64, v0: f64, v1: f64) -> GeopResult<NurbSurface3D> {
514 NurbSurface::revolve(&self.profile_curve(v0, v1)?, &self.axis()?, from, to)
515 }
516
517 pub fn iso(&self) -> Shape {
519 Shape::Iso {
520 origin: self.frame.origin,
521 axis: self.frame.z,
522 }
523 }
524
525 pub fn turn(&self, angle: f64) -> GeopResult<Pose> {
527 let axis = self.axis()?;
528 Pose::rotation_about(&axis.point, &axis.direction, Scalar::from_f64(angle))
529 }
530}
531
532impl NurbsCurve {
533 pub fn to_nurbs(&self) -> GeopResult<NurbCurve3D> {
534 let points = self
535 .points
536 .iter()
537 .enumerate()
538 .map(|(i, p)| homogeneous(*p, self.weights.as_ref().map(|w| w[i])))
539 .collect();
540 let curve = NurbCurve::try_new(
541 self.degree,
542 points,
543 self.knots.iter().map(|&k| Scalar::from_f64(k)).collect(),
544 )?;
545 clamped(curve)
546 }
547}
548
549fn clamped(curve: NurbCurve3D) -> GeopResult<NurbCurve3D> {
553 let p = curve.degree;
554 let k = &curve.knot_vector;
555 let n = k.len();
556 let is_clamped = (0..=p).all(|i| k[i].to_f64() == k[p].to_f64())
557 && (0..=p).all(|i| k[n - 1 - i].to_f64() == k[n - 1 - p].to_f64());
558 if is_clamped {
559 return Ok(curve);
560 }
561 let (lo, hi) = curve.domain();
562 curve.sub_curve(lo, hi)
563}
564
565fn homogeneous(p: P3, weight: Option<f64>) -> Vector4 {
566 match weight {
567 None => Vector4::from_array([
570 Scalar::from_f64(p[0]),
571 Scalar::from_f64(p[1]),
572 Scalar::from_f64(p[2]),
573 Scalar::ONE,
574 ]),
575 Some(w) => {
576 let w = Scalar::from_f64(w);
577 Vector4::from_array([
578 Scalar::from_f64(p[0]).mul(w),
579 Scalar::from_f64(p[1]).mul(w),
580 Scalar::from_f64(p[2]).mul(w),
581 w,
582 ])
583 }
584 }
585}
586
587impl NurbsSurface {
588 pub fn to_nurbs(&self) -> GeopResult<NurbSurface3D> {
589 let points = self
590 .points
591 .iter()
592 .enumerate()
593 .map(|(i, p)| homogeneous(*p, self.weights.as_ref().map(|w| w[i])))
594 .collect();
595 let surface = NurbSurface::try_new(
596 self.degree_u,
597 self.degree_v,
598 points,
599 self.knots_u.iter().map(|&k| Scalar::from_f64(k)).collect(),
600 self.knots_v.iter().map(|&k| Scalar::from_f64(k)).collect(),
601 )?;
602 let clamped_in = |knots: &[f64], p: usize| {
603 let n = knots.len();
604 (0..=p).all(|i| knots[i] == knots[p])
605 && (0..=p).all(|i| knots[n - 1 - i] == knots[n - 1 - p])
606 };
607 if clamped_in(&self.knots_u, self.degree_u) && clamped_in(&self.knots_v, self.degree_v) {
608 Ok(surface)
609 } else {
610 surface.sub_surface(surface.domain_u(), surface.domain_v())
611 }
612 }
613}
614
615impl CurveDef {
616 pub fn edge(
627 &self,
628 from: P3,
629 to: P3,
630 closed: bool,
631 uncertainty: f64,
632 ) -> GeopResult<NurbCurve3D> {
633 let s3 = |p: P3| Vector3::from_array(p.map(Scalar::from_f64));
634 match self {
635 CurveDef::Line { .. } => {
636 if closed {
637 return Err(GeopError::new(
638 "a straight edge cannot start and end at the same vertex",
639 ));
640 }
641 NurbCurve3D::line(s3(from), s3(to))
642 }
643 CurveDef::Circle { frame, radius } => Arc {
644 circle: Circle {
645 center: s3(frame.origin),
646 normal: s3(frame.z),
647 radius: Scalar::from_f64(*radius),
648 },
649 start: s3(from),
650 end: s3(to),
651 }
652 .to_curve(),
653 CurveDef::Ellipse { frame, a, b } => {
654 let unit = |p: P3| {
658 let l = frame.local(p);
659 let (s, c) = (l[1] / b).atan2(l[0] / a).sin_cos();
660 Vector3::from_array([Scalar::from_f64(c), Scalar::from_f64(s), Scalar::ZERO])
661 };
662 let (start, end) = (unit(from), unit(to));
663 let end = if closed { start } else { end };
664 let arc = Arc {
665 circle: Circle {
666 center: Vector3::zero(),
667 normal: Vector3::from_array([Scalar::ZERO, Scalar::ZERO, Scalar::ONE]),
668 radius: Scalar::ONE,
669 },
670 start,
671 end,
672 }
673 .to_curve()?;
674 let (o, x, y) = (
675 s3(frame.origin),
676 s3(scale(frame.x, *a)),
677 s3(scale(frame.y, *b)),
678 );
679 let control_points = arc
680 .control_points
681 .iter()
682 .map(|cp| {
683 let w = cp[3];
684 let p = o
685 .prod_scalar(w)
686 .add(&x.prod_scalar(cp[0]))
687 .add(&y.prod_scalar(cp[1]));
688 Vector4::from_array([p[0], p[1], p[2], w])
689 })
690 .collect();
691 NurbCurve::try_new(arc.degree, control_points, arc.knot_vector)
692 }
693 CurveDef::Nurbs(nurbs) => trim(nurbs.to_nurbs()?, from, to, closed, uncertainty),
694 CurveDef::Conic { frame, kind } => {
695 if closed {
696 return Err(GeopError::new(
697 "a hyperbola or parabola cannot close on itself",
698 ));
699 }
700 let parameter = |p: P3| {
703 let l = frame.local(p);
704 match kind {
705 ConicKind::Hyperbola { b, .. } => (l[1] / b).asinh(),
706 ConicKind::Parabola { focal } => l[1] / (2.0 * focal),
707 }
708 };
709 let (u0, u1) = (parameter(from), parameter(to));
710 let (middle, weight) = match kind {
711 ConicKind::Hyperbola { a, b } => {
712 let (m, h) = ((u0 + u1) / 2.0, (u1 - u0) / 2.0);
713 (
714 frame.point([a * m.cosh() / h.cosh(), b * m.sinh() / h.cosh(), 0.0]),
715 h.cosh(),
716 )
717 }
718 ConicKind::Parabola { focal } => {
719 (frame.point([focal * u0 * u1, focal * (u0 + u1), 0.0]), 1.0)
720 }
721 };
722 let control_points = vec![
723 homogeneous(from, None),
724 homogeneous(middle, (weight != 1.0).then_some(weight)),
725 homogeneous(to, None),
726 ];
727 NurbCurve::try_new(
728 2,
729 control_points,
730 vec![
731 Scalar::ZERO,
732 Scalar::ZERO,
733 Scalar::ZERO,
734 Scalar::ONE,
735 Scalar::ONE,
736 Scalar::ONE,
737 ],
738 )
739 }
740 CurveDef::Polyline(points) => {
741 let n = points.len();
742 if n < 2 {
743 return Err(GeopError::new("a polyline of fewer than two points"));
744 }
745 let mut knots = vec![Scalar::ZERO];
746 for i in 0..n {
747 knots.push(Scalar::from_ratio(i as i64, n as i64 - 1)?);
748 }
749 knots.push(Scalar::ONE);
750 let control_points = points.iter().map(|p| homogeneous(*p, None)).collect();
751 trim(
752 NurbCurve::try_new(1, control_points, knots)?,
753 from,
754 to,
755 closed,
756 uncertainty,
757 )
758 }
759 }
760 }
761}
762
763fn trim(
765 curve: NurbCurve3D,
766 from: P3,
767 to: P3,
768 closed: bool,
769 uncertainty: f64,
770) -> GeopResult<NurbCurve3D> {
771 let (lo, hi) = curve.domain();
772 let start = curve.evaluate(lo)?.to_f64_array();
773 let end = curve.evaluate(hi)?.to_f64_array();
774 let t0 = if distance(start, from) <= uncertainty {
775 None
776 } else {
777 Some(parameter_of(&curve, from)?)
778 };
779 let t1 = if distance(end, to) <= uncertainty || (closed && t0.is_none()) {
780 None
781 } else {
782 Some(parameter_of(&curve, to)?)
783 };
784 if t0.is_none() && t1.is_none() {
785 return Ok(curve);
786 }
787 let a = t0.unwrap_or(lo);
788 let b = t1.unwrap_or(hi);
789 if !a.definitely_less(b) {
790 return Err(GeopError::new(format!(
791 "the edge from {from:?} to {to:?} runs against its curve, or round past the curve's own ends (at parameters {a:?} and {b:?} of {lo:?} to {hi:?})"
792 )));
793 }
794 curve.sub_curve(a, b)
795}
796
797pub fn parameter_of(curve: &NurbCurve3D, p: P3) -> GeopResult<Scalar> {
801 let target = Vector3::from_array(p.map(Scalar::from_f64));
802 Ok(curve.closest_point(&target)?.0.sharpen())
803}
804
805impl SurfaceDef {
806 pub fn revolved(&self) -> Option<&Revolved> {
809 match &self.kind {
810 SurfaceKind::Revolved(r) => Some(r),
811 _ => None,
812 }
813 }
814}
815
816impl<'a> Reader<'a> {
817 pub fn point(&self, scope: &Scope, id: u64) -> GeopResult<P3> {
818 let args = self.args(id, "CARTESIAN_POINT")?;
819 let c = args.reals(1)?;
820 if c.len() != 3 {
821 return Err(GeopError::new(format!(
822 "#{id} CARTESIAN_POINT has {} coordinates, where 3 were expected",
823 c.len()
824 )));
825 }
826 Ok(scope.place.point(scale([c[0], c[1], c[2]], scope.length)))
827 }
828
829 pub fn direction(&self, scope: &Scope, id: u64) -> GeopResult<P3> {
830 let args = self.args(id, "DIRECTION")?;
831 let c = args.reals(1)?;
832 if c.len() != 3 {
833 return Err(GeopError::new(format!(
834 "#{id} DIRECTION has {} components, where 3 were expected",
835 c.len()
836 )));
837 }
838 let d = normalize([c[0], c[1], c[2]])
839 .ok_or_else(|| GeopError::new(format!("#{id} DIRECTION has no length")))?;
840 Ok(scope.place.direction(d))
841 }
842
843 pub fn vector(&self, scope: &Scope, id: u64) -> GeopResult<P3> {
845 let args = self.args(id, "VECTOR")?;
846 let d = self.direction(scope, args.reference(1)?)?;
847 Ok(scale(d, args.real(2)? * scope.length))
848 }
849
850 pub fn frame(&self, scope: &Scope, id: u64) -> GeopResult<Frame> {
851 let instance = self.instance(id)?;
852 if instance.is("AXIS2_PLACEMENT_2D") {
853 return Err(unsupported(
854 id,
855 instance,
856 "a 2-D placement where a 3-D one is needed",
857 ));
858 }
859 let args = self.args(id, "AXIS2_PLACEMENT_3D")?;
860 let origin = self.point(scope, args.reference(1)?)?;
861 let axis = if args.is_null(2) {
862 scope.place.direction([0.0, 0.0, 1.0])
863 } else {
864 self.direction(scope, args.reference(2)?)?
865 };
866 let reference = if args.is_null(3) {
867 scope.place.direction([1.0, 0.0, 0.0])
868 } else {
869 self.direction(scope, args.reference(3)?)?
870 };
871 Frame::new(origin, axis, reference)
872 .map_err(|e| e.with_context(format!("#{id} AXIS2_PLACEMENT_3D")))
873 }
874
875 pub fn curve(&self, scope: &Scope, id: u64) -> GeopResult<(CurveDef, bool)> {
878 let instance = self.instance(id)?;
879 if instance.is("B_SPLINE_CURVE")
880 || instance.is("B_SPLINE_CURVE_WITH_KNOTS")
881 || instance.is("BEZIER_CURVE")
882 || instance.is("UNIFORM_CURVE")
883 || instance.is("QUASI_UNIFORM_CURVE")
884 || instance.is("RATIONAL_B_SPLINE_CURVE")
885 {
886 return Ok((CurveDef::Nurbs(self.nurbs_curve(scope, id)?), false));
887 }
888 let (kind, args) = self
889 .args_of_any(
890 id,
891 &[
892 "LINE",
893 "CIRCLE",
894 "ELLIPSE",
895 "TRIMMED_CURVE",
896 "SURFACE_CURVE",
897 "SEAM_CURVE",
898 "INTERSECTION_CURVE",
899 "POLYLINE",
900 "HYPERBOLA",
901 "PARABOLA",
902 ],
903 )
904 .map_err(|_| unsupported(id, instance, "this kind of curve cannot be read"))?;
905 Ok(match kind {
906 "LINE" => (
907 CurveDef::Line {
908 origin: self.point(scope, args.reference(1)?)?,
909 direction: self.vector(scope, args.reference(2)?)?,
910 },
911 false,
912 ),
913 "CIRCLE" => (
914 CurveDef::Circle {
915 frame: self.frame(scope, args.reference(1)?)?,
916 radius: args.real(2)? * scope.length,
917 },
918 false,
919 ),
920 "ELLIPSE" => (
921 CurveDef::Ellipse {
922 frame: self.frame(scope, args.reference(1)?)?,
923 a: args.real(2)? * scope.length,
924 b: args.real(3)? * scope.length,
925 },
926 false,
927 ),
928 "TRIMMED_CURVE" => {
929 let (basis, reversed) = self.curve(scope, args.reference(1)?)?;
932 (basis, reversed == args.logical(4)?)
933 }
934 "HYPERBOLA" => (
935 CurveDef::Conic {
936 frame: self.frame(scope, args.reference(1)?)?,
937 kind: ConicKind::Hyperbola {
938 a: args.real(2)? * scope.length,
939 b: args.real(3)? * scope.length,
940 },
941 },
942 false,
943 ),
944 "PARABOLA" => (
945 CurveDef::Conic {
946 frame: self.frame(scope, args.reference(1)?)?,
947 kind: ConicKind::Parabola {
948 focal: args.real(2)? * scope.length,
949 },
950 },
951 false,
952 ),
953 "POLYLINE" => (
954 CurveDef::Polyline(
955 args.references(1)?
956 .into_iter()
957 .map(|p| self.point(scope, p))
958 .collect::<GeopResult<_>>()?,
959 ),
960 false,
961 ),
962 _ => self.curve(scope, args.reference(1)?)?,
965 })
966 }
967
968 fn nurbs_curve(&self, scope: &Scope, id: u64) -> GeopResult<NurbsCurve> {
969 let instance = self.instance(id)?;
970 let (base, own): (Args, Option<Args>) = match instance {
973 crate::part21::Instance::Simple(record) => {
974 let args = Args { id, record };
975 (args, Some(args))
976 }
977 crate::part21::Instance::Complex(_) => (self.args(id, "B_SPLINE_CURVE")?, None),
978 };
979 let offset = usize::from(own.is_some());
980 let degree = base.integer(offset)? as usize;
981 let points: Vec<P3> = base
982 .references(offset + 1)?
983 .into_iter()
984 .map(|p| self.point(scope, p))
985 .collect::<GeopResult<_>>()?;
986 let n = points.len();
987 let subtype = |name: &str| -> Option<Args> {
988 match own {
989 Some(args) if args.record.name == name => Some(args),
990 Some(_) => None,
991 None => instance.record(name).map(|record| Args { id, record }),
992 }
993 };
994 let sub_offset = if own.is_some() { 6 } else { 0 };
997 let knots = if let Some(args) = subtype("B_SPLINE_CURVE_WITH_KNOTS") {
998 expand_knots(
999 id,
1000 &args.integers(sub_offset)?,
1001 &args.reals(sub_offset + 1)?,
1002 )?
1003 } else if subtype("BEZIER_CURVE").is_some() {
1004 bezier_knots(id, n, degree)?
1005 } else if subtype("QUASI_UNIFORM_CURVE").is_some() {
1006 quasi_uniform_knots(n, degree)
1007 } else if subtype("UNIFORM_CURVE").is_some() {
1008 (0..n + degree + 1)
1009 .map(|i| i as f64 - degree as f64)
1010 .collect()
1011 } else {
1012 return Err(unsupported(
1013 id,
1014 instance,
1015 "a B-spline curve without a knot vector",
1016 ));
1017 };
1018 if knots.len() != n + degree + 1 {
1019 return Err(GeopError::new(format!(
1020 "#{id} {}: {} knots for {n} control points of degree {degree}, where {} were expected",
1021 instance.type_name(),
1022 knots.len(),
1023 n + degree + 1
1024 )));
1025 }
1026 let weights = match instance.record("RATIONAL_B_SPLINE_CURVE") {
1027 Some(record) => {
1028 let args = Args { id, record };
1029 let w = args.reals(if own.is_some() { 6 } else { 0 })?;
1030 check_weights(id, &w, n)?;
1031 Some(w)
1032 }
1033 None => None,
1034 };
1035 Ok(NurbsCurve {
1036 degree,
1037 points,
1038 weights,
1039 knots,
1040 })
1041 }
1042
1043 pub fn surface(&self, scope: &Scope, id: u64) -> GeopResult<SurfaceDef> {
1044 let instance = self.instance(id)?;
1045 if instance.is("B_SPLINE_SURFACE")
1046 || instance.is("B_SPLINE_SURFACE_WITH_KNOTS")
1047 || instance.is("BEZIER_SURFACE")
1048 || instance.is("UNIFORM_SURFACE")
1049 || instance.is("QUASI_UNIFORM_SURFACE")
1050 {
1051 return Ok(SurfaceDef {
1052 kind: SurfaceKind::Nurbs(self.nurbs_surface(scope, id)?),
1053 flipped: false,
1054 });
1055 }
1056 let (kind, args) = self
1057 .args_of_any(
1058 id,
1059 &[
1060 "PLANE",
1061 "CYLINDRICAL_SURFACE",
1062 "CONICAL_SURFACE",
1063 "SPHERICAL_SURFACE",
1064 "TOROIDAL_SURFACE",
1065 "SURFACE_OF_REVOLUTION",
1066 "SURFACE_OF_LINEAR_EXTRUSION",
1067 "RECTANGULAR_TRIMMED_SURFACE",
1068 ],
1069 )
1070 .map_err(|_| unsupported(id, instance, "this kind of surface cannot be read"))?;
1071 let revolved = |profile: Profile| -> GeopResult<SurfaceDef> {
1072 Ok(SurfaceDef {
1073 kind: SurfaceKind::Revolved(Revolved {
1074 frame: self.frame(scope, args.reference(1)?)?,
1075 profile,
1076 on_axis: scope.uncertainty,
1077 }),
1078 flipped: false,
1079 })
1080 };
1081 let length = |k: usize| -> GeopResult<f64> { Ok(args.real(k)? * scope.length) };
1082 let positive = |k: usize, what: &str| -> GeopResult<f64> {
1083 let value = length(k)?;
1084 if value > 0.0 {
1085 Ok(value)
1086 } else {
1087 Err(unsupported(
1088 id,
1089 instance,
1090 &format!("its {what} {value} is not positive"),
1091 ))
1092 }
1093 };
1094 match kind {
1095 "PLANE" => Ok(SurfaceDef {
1096 kind: SurfaceKind::Plane(self.frame(scope, args.reference(1)?)?),
1097 flipped: false,
1098 }),
1099 "CYLINDRICAL_SURFACE" => revolved(Profile::Line {
1100 rho: positive(2, "radius")?,
1101 h: 0.0,
1102 drho: 0.0,
1103 dh: 1.0,
1104 }),
1105 "CONICAL_SURFACE" => {
1106 let angle = args.real(3)? * scope.angle;
1107 if !(angle > 0.0 && angle < std::f64::consts::FRAC_PI_2) {
1108 return Err(unsupported(
1109 id,
1110 instance,
1111 &format!(
1112 "its half angle {angle} rad is not between none and a right angle"
1113 ),
1114 ));
1115 }
1116 revolved(Profile::Line {
1117 rho: length(2)?,
1118 h: 0.0,
1119 drho: angle.tan(),
1120 dh: 1.0,
1121 })
1122 }
1123 "SPHERICAL_SURFACE" => revolved(Profile::Sphere {
1124 radius: positive(2, "radius")?,
1125 }),
1126 "TOROIDAL_SURFACE" => {
1127 let major = positive(2, "major radius")?;
1128 let minor = positive(3, "minor radius")?;
1129 revolved(Profile::Circle {
1130 rho: major,
1131 h: 0.0,
1132 radius: minor,
1133 })
1134 }
1135 "SURFACE_OF_REVOLUTION" => {
1136 let (curve, reversed) = self.curve(scope, args.reference(1)?)?;
1137 let axis = self.args(args.reference(2)?, "AXIS1_PLACEMENT")?;
1138 let origin = self.point(scope, axis.reference(1)?)?;
1139 let z = if axis.is_null(2) {
1140 scope.place.direction([0.0, 0.0, 1.0])
1141 } else {
1142 self.direction(scope, axis.reference(2)?)?
1143 };
1144 let radial = |p: P3| {
1145 let d = sub(p, origin);
1146 sub(d, scale(z, dot(d, z)))
1147 };
1148 let off_plane = || {
1149 unsupported(
1150 id,
1151 instance,
1152 "a surface of revolution whose curve does not lie in a plane through its axis",
1153 )
1154 };
1155 let (frame, profile, along) = match curve {
1161 CurveDef::Nurbs(curve) => {
1162 let off_axis = curve
1164 .points
1165 .iter()
1166 .map(|&p| radial(p))
1167 .max_by(|a, b| norm(*a).total_cmp(&norm(*b)))
1168 .unwrap_or([0.0; 3]);
1169 let frame = Frame::new(origin, z, off_axis)?;
1170 for &p in &curve.points {
1171 let l = frame.local(p);
1172 if l[1].abs() > scope.uncertainty || l[0] < -scope.uncertainty {
1173 return Err(unsupported(
1174 id,
1175 instance,
1176 "a surface of revolution whose curve does not lie in one half plane through its axis",
1177 ));
1178 }
1179 }
1180 (frame, Profile::Curve(curve), true)
1181 }
1182 CurveDef::Line {
1183 origin: p0,
1184 direction,
1185 } => {
1186 let d = normalize(direction).ok_or_else(off_plane)?;
1187 let off_axis = if norm(radial(p0)) > scope.uncertainty {
1188 radial(p0)
1189 } else {
1190 radial(add(p0, d))
1191 };
1192 let frame = Frame::new(origin, z, off_axis)?;
1193 let (l0, l1) = (frame.local(p0), frame.local(add(p0, d)));
1194 if l0[1].abs() > scope.uncertainty || l1[1].abs() > scope.uncertainty {
1195 return Err(off_plane());
1196 }
1197 let profile = Profile::Line {
1198 rho: l0[0],
1199 h: l0[2],
1200 drho: l1[0] - l0[0],
1201 dh: l1[2] - l0[2],
1202 };
1203 (frame, profile, true)
1204 }
1205 CurveDef::Circle {
1206 frame: circle,
1207 radius,
1208 } => {
1209 let center = radial(circle.origin);
1210 let on_axis = norm(center) <= scope.uncertainty;
1211 let reference = if on_axis {
1212 radial(add(circle.origin, circle.x))
1213 } else {
1214 center
1215 };
1216 let frame = Frame::new(origin, z, reference)?;
1217 let c = frame.local(circle.origin);
1218 let n = [
1219 dot(circle.z, frame.x),
1220 dot(circle.z, frame.y),
1221 dot(circle.z, frame.z),
1222 ];
1223 if c[1].abs() > scope.uncertainty || n[0].abs() > 1e-9 || n[2].abs() > 1e-9
1224 {
1225 return Err(off_plane());
1226 }
1227 let along = n[1] < 0.0;
1229 if on_axis {
1230 let frame = Frame {
1231 origin: frame.point([0.0, 0.0, c[2]]),
1232 ..frame
1233 };
1234 (frame, Profile::Sphere { radius }, along)
1235 } else {
1236 (
1237 frame,
1238 Profile::Circle {
1239 rho: c[0],
1240 h: c[2],
1241 radius,
1242 },
1243 along,
1244 )
1245 }
1246 }
1247 CurveDef::Ellipse { .. } | CurveDef::Polyline(_) | CurveDef::Conic { .. } => {
1248 return Err(unsupported(
1249 id,
1250 instance,
1251 "a surface of revolution of an ellipse, a conic or a polyline",
1252 ));
1253 }
1254 };
1255 Ok(SurfaceDef {
1256 kind: SurfaceKind::Revolved(Revolved {
1257 frame,
1258 profile,
1259 on_axis: scope.uncertainty,
1260 }),
1261 flipped: along == reversed,
1262 })
1263 }
1264 "SURFACE_OF_LINEAR_EXTRUSION" => {
1265 let (curve, _) = self.curve(scope, args.reference(1)?)?;
1266 let curve = match curve {
1267 CurveDef::Nurbs(nurbs) => nurbs,
1268 _ => {
1269 return Err(unsupported(
1270 id,
1271 instance,
1272 "a surface of extrusion of a curve other than a B-spline",
1273 ));
1274 }
1275 };
1276 Ok(SurfaceDef {
1277 kind: SurfaceKind::Extrusion {
1278 curve,
1279 vector: self.vector(scope, args.reference(2)?)?,
1280 },
1281 flipped: false,
1282 })
1283 }
1284 "RECTANGULAR_TRIMMED_SURFACE" => {
1285 let mut basis = self.surface(scope, args.reference(1)?)?;
1288 let turned = args.logical(6)? != args.logical(7)?;
1289 basis.flipped ^= turned;
1290 Ok(basis)
1291 }
1292 _ => unreachable!("one of the kinds asked for"),
1293 }
1294 }
1295
1296 fn nurbs_surface(&self, scope: &Scope, id: u64) -> GeopResult<NurbsSurface> {
1297 let instance = self.instance(id)?;
1298 let simple = matches!(instance, crate::part21::Instance::Simple(_));
1299 let base = if simple {
1300 match instance {
1301 crate::part21::Instance::Simple(record) => Args { id, record },
1302 _ => unreachable!(),
1303 }
1304 } else {
1305 self.args(id, "B_SPLINE_SURFACE")?
1306 };
1307 let offset = usize::from(simple);
1308 let degree_u = base.integer(offset)? as usize;
1309 let degree_v = base.integer(offset + 1)? as usize;
1310 let rows = base.list(offset + 2)?;
1311 let mut points = Vec::new();
1312 let mut num_v = None;
1313 for row in rows {
1314 let crate::part21::Value::List(row) = row else {
1315 return Err(GeopError::new(format!(
1316 "#{id} {}: its control points are not a list of rows",
1317 instance.type_name()
1318 )));
1319 };
1320 if *num_v.get_or_insert(row.len()) != row.len() {
1321 return Err(GeopError::new(format!(
1322 "#{id} {}: its rows of control points differ in length",
1323 instance.type_name()
1324 )));
1325 }
1326 for p in row {
1327 let crate::part21::Value::Ref(p) = p else {
1328 return Err(GeopError::new(format!(
1329 "#{id} {}: a control point is not a reference",
1330 instance.type_name()
1331 )));
1332 };
1333 points.push(self.point(scope, *p)?);
1334 }
1335 }
1336 let num_u = rows.len();
1337 let num_v = num_v.unwrap_or(0);
1338 let subtype = |name: &str| -> Option<Args> {
1339 if simple {
1340 (base.record.name == name).then_some(base)
1341 } else {
1342 instance.record(name).map(|record| Args { id, record })
1343 }
1344 };
1345 let sub_offset = if simple { 8 } else { 0 };
1346 let (knots_u, knots_v) = if let Some(args) = subtype("B_SPLINE_SURFACE_WITH_KNOTS") {
1347 (
1348 expand_knots(
1349 id,
1350 &args.integers(sub_offset)?,
1351 &args.reals(sub_offset + 2)?,
1352 )?,
1353 expand_knots(
1354 id,
1355 &args.integers(sub_offset + 1)?,
1356 &args.reals(sub_offset + 3)?,
1357 )?,
1358 )
1359 } else if subtype("BEZIER_SURFACE").is_some() {
1360 (
1361 bezier_knots(id, num_u, degree_u)?,
1362 bezier_knots(id, num_v, degree_v)?,
1363 )
1364 } else if subtype("QUASI_UNIFORM_SURFACE").is_some() {
1365 (
1366 quasi_uniform_knots(num_u, degree_u),
1367 quasi_uniform_knots(num_v, degree_v),
1368 )
1369 } else {
1370 return Err(unsupported(
1371 id,
1372 instance,
1373 "a B-spline surface without a knot vector",
1374 ));
1375 };
1376 for (knots, n, p, dir) in [
1377 (&knots_u, num_u, degree_u, "u"),
1378 (&knots_v, num_v, degree_v, "v"),
1379 ] {
1380 if knots.len() != n + p + 1 {
1381 return Err(GeopError::new(format!(
1382 "#{id} {}: {} knots in {dir} for {n} control points of degree {p}, where {} were expected",
1383 instance.type_name(),
1384 knots.len(),
1385 n + p + 1
1386 )));
1387 }
1388 }
1389 let weights = match instance.record("RATIONAL_B_SPLINE_SURFACE") {
1390 Some(record) => {
1391 let args = Args { id, record };
1392 let k = if simple { 8 } else { 0 };
1393 let mut w = Vec::new();
1394 for row in args.list(k)? {
1395 let crate::part21::Value::List(row) = row else {
1396 return Err(GeopError::new(format!(
1397 "#{id}: weights are not a list of rows"
1398 )));
1399 };
1400 for x in row {
1401 w.push(super::reader::real(x).ok_or_else(|| {
1402 GeopError::new(format!("#{id}: a weight is not a number"))
1403 })?);
1404 }
1405 }
1406 check_weights(id, &w, num_u * num_v)?;
1407 Some(w)
1408 }
1409 None => None,
1410 };
1411 Ok(NurbsSurface {
1412 degree_u,
1413 degree_v,
1414 num_u,
1415 num_v,
1416 points,
1417 weights,
1418 knots_u,
1419 knots_v,
1420 })
1421 }
1422}
1423
1424fn check_weights(id: u64, weights: &[f64], n: usize) -> GeopResult<()> {
1425 if weights.len() != n {
1426 return Err(GeopError::new(format!(
1427 "#{id}: {} weights for {n} control points",
1428 weights.len()
1429 )));
1430 }
1431 if let Some(w) = weights.iter().find(|w| w.is_nan() || **w <= 0.0) {
1432 return Err(GeopError::new(format!(
1433 "#{id}: the weight {w} is not positive"
1434 )));
1435 }
1436 Ok(())
1437}
1438
1439fn expand_knots(id: u64, multiplicities: &[i64], knots: &[f64]) -> GeopResult<Vec<f64>> {
1440 if multiplicities.len() != knots.len() {
1441 return Err(GeopError::new(format!(
1442 "#{id}: {} knot multiplicities for {} knots",
1443 multiplicities.len(),
1444 knots.len()
1445 )));
1446 }
1447 let mut out = Vec::new();
1448 for (&m, &k) in multiplicities.iter().zip(knots) {
1449 if m < 1 {
1450 return Err(GeopError::new(format!("#{id}: a knot multiplicity of {m}")));
1451 }
1452 if out.last().is_some_and(|&last| last > k) {
1453 return Err(GeopError::new(format!("#{id}: its knots decrease")));
1454 }
1455 out.extend(std::iter::repeat_n(k, m as usize));
1456 }
1457 Ok(out)
1458}
1459
1460fn bezier_knots(id: u64, n: usize, degree: usize) -> GeopResult<Vec<f64>> {
1461 if degree == 0 || !(n - 1).is_multiple_of(degree) {
1462 return Err(GeopError::new(format!(
1463 "#{id}: {n} control points do not make Bezier pieces of degree {degree}"
1464 )));
1465 }
1466 let pieces = (n - 1) / degree;
1467 let mut knots = vec![0.0; degree + 1];
1468 for k in 1..pieces {
1469 knots.extend(std::iter::repeat_n(k as f64, degree));
1470 }
1471 knots.extend(std::iter::repeat_n(pieces as f64, degree + 1));
1472 Ok(knots)
1473}
1474
1475fn quasi_uniform_knots(n: usize, degree: usize) -> Vec<f64> {
1476 let mut knots = vec![0.0; degree + 1];
1477 let inner = n.saturating_sub(degree + 1);
1478 for k in 1..=inner {
1479 knots.push(k as f64);
1480 }
1481 knots.extend(std::iter::repeat_n((inner + 1) as f64, degree + 1));
1482 knots
1483}
1484
1485pub fn point_at(curve: &NurbCurve3D, t: f64) -> GeopResult<P3> {
1488 let (lo, hi) = curve.domain();
1489 let t = if t <= lo.to_f64() {
1490 lo
1491 } else if t >= hi.to_f64() {
1492 hi
1493 } else {
1494 Scalar::from_f64(t)
1495 };
1496 Ok(curve.evaluate(t)?.to_f64_array())
1497}