Skip to main content

geop_ops_step/import/
geometry.rs

1//! The geometry of a STEP file: points, placements, curves and surfaces as
2//! the file defines them — in millimetres, placed where the assembly puts
3//! them — and their conversion to the kernel's NURBS.
4//!
5//! What the file says is read into plain `f64` definitions ([`CurveDef`],
6//! [`SurfaceDef`]): it is data, decided on (where a face wraps around, which
7//! loop is outer) before anything exact is built from it. The NURBS built
8//! from it are exact for every analytic type — lines, circles, ellipses,
9//! planes, cylinders, cones, spheres, tori, surfaces of revolution and of
10//! extrusion — not approximations.
11
12use 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/// A right-handed orthonormal frame: `AXIS2_PLACEMENT_3D`.
28#[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    /// The frame at `origin` with its `z` along `axis` and its `x` along
38    /// `reference` made perpendicular to it — or along any perpendicular,
39    /// if `reference` is parallel to `axis`.
40    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    /// `p`, given in this frame's coordinates.
62    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    /// `p` in this frame's coordinates.
73    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/// A rigid motion, as an assembly places a part: `p -> origin + axes p`.
80#[derive(Clone, Copy, Debug, PartialEq)]
81pub struct Placement {
82    /// The images of the unit vectors: the columns of the rotation.
83    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    /// The motion taking the world frame to `frame`.
94    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    /// This motion, then `outer`.
113    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        // The transpose of an orthonormal rotation is its inverse.
122        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/// How the geometry of one representation is read: its units, its
136/// uncertainty, and where the assembly places it.
137#[derive(Clone, Copy, Debug)]
138pub struct Scope {
139    /// Millimetres per length unit of the file.
140    pub length: f64,
141    /// Radians per angle unit of the file.
142    pub angle: f64,
143    /// How far apart two points the file means as one may be, in
144    /// millimetres: the file's own `distance_accuracy_value`.
145    pub uncertainty: f64,
146    pub place: Placement,
147}
148
149/// A curve as the file defines it.
150#[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    /// A hyperbola or a parabola, in the plane of `frame`.
168    Conic {
169        frame: Frame,
170        kind: ConicKind,
171    },
172}
173
174/// Where a curve the file defines is a circle or a line, what tells
175/// whether it runs along a parallel or a meridian of a surface of
176/// revolution (see [`Revolved::iso_line`]).
177#[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    /// Built as a parallel or a meridian of the surface of revolution about
189    /// the axis through `origin` along `axis` — by the importer itself,
190    /// cutting a face that wraps round it.
191    Iso {
192        origin: P3,
193        axis: P3,
194    },
195}
196
197impl CurveDef {
198    /// Whether it is a circle or a line, and which.
199    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    /// `a cosh u x + b sinh u y`.
218    Hyperbola { a: f64, b: f64 },
219    /// `focal (u^2 x + 2 u y)`.
220    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/// A surface as the file defines it, and whether the NURBS it is built as
232/// (see [`SurfaceDef::natural_normal_flipped`]) faces the other way.
233#[derive(Clone, Debug)]
234pub struct SurfaceDef {
235    pub kind: SurfaceKind,
236    /// Whether the surface's normal as the file defines it is opposite to
237    /// that of the NURBS built from it: what decides, with a face's own
238    /// `same_sense`, which way a face's NURBS has to run.
239    pub flipped: bool,
240}
241
242#[derive(Clone, Debug)]
243pub enum SurfaceKind {
244    Plane(Frame),
245    Revolved(Revolved),
246    Nurbs(NurbsSurface),
247    /// `curve` swept along `vector`: `Scalar(u, v) = curve(u) + v vector`.
248    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    /// Row-major, `u` the row.
261    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/// A surface swept by a profile turning about the `z` of `frame`: the
268/// profile lies in the half plane of `frame.x` at angle 0.
269#[derive(Clone, Debug)]
270pub struct Revolved {
271    pub frame: Frame,
272    pub profile: Profile,
273    /// How close to the axis a point is on it, and has no angle: the
274    /// file's uncertainty.
275    pub on_axis: f64,
276}
277
278/// What a [`Revolved`] surface's profile is, as a function of its
279/// parameter `v` in the half plane at angle 0: `(radius, height)` along
280/// `frame.x` and `frame.z`.
281#[derive(Clone, Debug)]
282pub enum Profile {
283    /// A straight line, `(rho + v drho, h + v dh)`: a cylinder's, a
284    /// cone's — its apex where the radius is none — or any line's.
285    Line {
286        rho: f64,
287        h: f64,
288        drho: f64,
289        dh: f64,
290    },
291    /// `(r cos v, r sin v)`, `v` from `-pi/2` to `pi/2`.
292    Sphere { radius: f64 },
293    /// A circle off the axis, `(rho + r cos v, h + r sin v)`: a torus'
294    /// tube, or any circle's. One reaching across the axis (`rho <= r`)
295    /// makes two sheets meeting at the poles where it crosses the axis: its
296    /// arc on this side turned, `v` between the poles — an apple — and the
297    /// arc past the axis turned, a lemon inside it (see
298    /// [`Revolved::past_axis`]).
299    Circle { rho: f64, h: f64, radius: f64 },
300    /// A curve of its own: `v` its parameter.
301    Curve(NurbsCurve),
302}
303
304impl Revolved {
305    /// The angle of `p` about the axis, and its profile parameter where
306    /// the profile is analytic — `None` for the angle of a point on the
307    /// axis, which has none.
308    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                // A line crossing the axis — a cone's — reaches past its
320                // apex to negative radii: a point there is the profile's at
321                // the radius taken negative, half a turn round. Whichever
322                // of the two lies on the line is the point's.
323                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    /// Whether a curve of `shape` lying on this surface runs along a line
346    /// of one parameter of its patches, to within `uncertainty`, the file's
347    /// statement of which places are one: a circle about its axis is a
348    /// parallel, at one profile parameter, and a line in a plane through
349    /// its axis a meridian, at one angle. On a patch its pcurve is then
350    /// straight.
351    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                // Its plane square to the axis — tilted by no more than
361                // the uncertainty at its rim — and its centre on the axis.
362                norm(cross(normal, z)) * radius <= uncertainty
363                    && norm(cross(sub(center, o), z)) <= uncertainty
364            }
365            Shape::Line { origin, direction } => {
366                // In a plane with the axis: no further from it, across
367                // both, than the uncertainty — `across` as long as the sine
368                // of the angle between them, none for a line along it.
369                let across = cross(direction, z);
370                dot(sub(origin, o), across).abs() <= uncertainty * norm(across)
371            }
372        }
373    }
374
375    /// For a circle crossing the axis, how much nearer `p` lies to the
376    /// sheet its arc past the axis turns into than to the one its arc on
377    /// this side does: positive where `p` is on the sheet past the axis.
378    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    /// The sheet the arc of a circle crossing the axis past it turns into,
397    /// as a surface of its own: the circle mirrored in the axis, turned on
398    /// this side. Its profile parameter runs the other way round the
399    /// circle, so its normal is the other way round to the file's.
400    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    /// Whether the profile's parameter `v` goes once round, as a torus'
416    /// does: then it is an angle too.
417    pub fn v_is_angle(&self) -> bool {
418        matches!(self.profile, Profile::Circle { rho, radius, .. } if rho > radius)
419    }
420
421    /// Where the profile meets the axis, by its parameter: the poles of the
422    /// surface.
423    pub fn poles(&self) -> Vec<f64> {
424        match &self.profile {
425            // A circle reaching the axis meets it where its radius is none,
426            // either side of its far point from the axis: the surface is
427            // the arc between, turned.
428            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    /// The profile's point at `v`, in the half plane at angle 0.
442    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    /// The profile from `v0` to `v1` as a curve, in the half plane at
453    /// angle 0 — the whole curve, for a profile that is one.
454    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                // Turning from `x` towards `z` is right-handed about `-y`.
466                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    /// The parallel at the profile parameter `v` — the circle about the
482    /// axis the profile's point there turns along — from the angle `from`
483    /// counter-clockwise about the axis to `to`: all the way round where
484    /// they are one.
485    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    /// The surface between the angles `from` and `to` and, where the
512    /// profile is analytic, the profile parameters `v0` and `v1`.
513    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    /// What a parallel or a meridian of it is, as an edge's [`Shape`].
518    pub fn iso(&self) -> Shape {
519        Shape::Iso {
520            origin: self.frame.origin,
521            axis: self.frame.z,
522        }
523    }
524
525    /// The motion turning by `angle` radians about the axis.
526    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
549/// `curve` with its ends at its first and last control points, as every
550/// curve of the kernel has: an unclamped (periodic) knot vector cut at the
551/// ends of its domain.
552fn 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        // A weight of one is taken as it is: multiplying by it would only
568        // widen the point by rounding.
569        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    /// The curve from `from` to `to` — points on it, the curve running from
617    /// one to the other the way it runs — as the kernel builds an edge: a
618    /// line or an arc ending exactly at those points, or the piece of a
619    /// B-spline between where they are on it. `closed` asks for the whole
620    /// way round, from a point back to itself.
621    ///
622    /// Where on a B-spline the points are is found by projecting them onto
623    /// it; within `uncertainty` of an end of the curve, they are taken to be
624    /// that end. That is the file's own statement of which points are one:
625    /// the trim it writes at an end means the end.
626    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                // The arc of the unit circle between the points' angles,
655                // stretched onto the ellipse: an affine map of a rational
656                // curve's control points is exact.
657                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                // One rational quadratic piece from `from` to `to`: its
701                // middle control point where the tangents at the ends meet.
702                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
763/// The piece of `curve` from `from` to `to` (see [`CurveDef::edge`]).
764fn 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
797/// Where on `curve` the point `p` is: the parameter of its nearest point
798/// ([`NurbCurve3D::closest_point`]). A free choice of where to cut the
799/// curve, so sharp.
800pub 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    /// Whether this is a plane or a surface of revolution, whose patches
807    /// are built to fit each face.
808    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    /// A `VECTOR`, its magnitude a length.
844    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    /// The curve `#id`, and whether the curve as used runs against the one
876    /// it is defined by (a `TRIMMED_CURVE` whose sense disagrees).
877    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                // The edge's own vertices say where it ends: only the basis
930                // curve, and which way round it is taken, matter.
931                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            // A curve on surfaces: its 3-D curve. The curves it has on the
963            // surfaces are fitted anew, as the kernel fits every pcurve.
964            _ => 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        // A simple instance holds every supertype's parameters before its
971        // own: name, degree, points, form, closed, self-intersecting.
972        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        // Parameters of a subtype start after the supertype's in a simple
995        // instance, at the start of its own record in a complex one.
996        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                // The file's surface is parametrized by the turn, then the
1156                // curve (ISO 10303-42), its normal the turn across the
1157                // curve's tangent; the NURBS' is the turn across the
1158                // profile's — so they agree where the profile runs the
1159                // curve's way.
1160                let (frame, profile, along) = match curve {
1161                    CurveDef::Nurbs(curve) => {
1162                        // Angle 0 is the half plane the profile lies in.
1163                        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                        // Turning from `x` towards `z` is about `-y`.
1228                        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                // The faces' bounds trim it: only the basis surface, and
1286                // whether its normal is turned, matter.
1287                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
1485/// The point of `curve` at the parameter `t`, chosen in `f64`: at a domain
1486/// end, the end itself, which `t` may miss by rounding.
1487pub 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}