Skip to main content

ogeom_io/
brep.rs

1//! The `.brep` interchange text format, both directions.
2//!
3//! An interchange format for boundary representation that a good deal of the
4//! field can already read, implemented here from its published
5//! specification. It is worth having for one reason: it is the cheapest
6//! bridge that exists for an application moving onto this kernel, far
7//! cheaper than STEP to produce and far more faithful than a mesh.
8//!
9//! The file is five sections (a header, a table of placements, a table of
10//! geometry, and a table of topology), and the last of those is the
11//! interesting one. Topology records are written leaves first and *numbered
12//! backwards*: the final record is number one, and a record refers to its
13//! children by how far above it they sit. So a parent can only name children
14//! already written, the file needs no forward references, and a reader can
15//! build the model in a single pass with nothing left dangling.
16//!
17//! ## What crosses
18//!
19//! Everything this kernel's own geometry can say: lines, circles, ellipses,
20//! parabolas, hyperbolas, B-splines, and the trimmed and offset forms over
21//! them, in two dimensions and three; planes, cylinders, cones, spheres,
22//! tori, extrusions, revolutions, B-spline surfaces, and trimmed and offset
23//! forms over those. All of the topology, with every occurrence's
24//! orientation and placement, and edges carrying their curve, their pcurves
25//! and their seams.
26//!
27//! ## What does not
28//!
29//! One thing the reader deliberately does *not* take on faith: whether an
30//! edge's representations agree on parameterization. That is a claim, the
31//! writing kernel's claim about its own data, and everything downstream
32//! relies on it, so it is measured here instead, each representation
33//! evaluated against the others at matched parameters and the claim
34//! re-established only where they actually land together. A file can
35//! therefore come back with the claim *established* where its writer never
36//! made it, which is the right direction to be wrong in.
37//!
38//! Cached triangulations and the polygons that go with them are parsed and
39//! skipped rather than half-honoured: this kernel tessellates on demand and
40//! keeps its own cache, and a mesh read from a file would be a mesh nobody
41//! could say the deflection of. The bookkeeping flags each record carries
42//! are read and dropped for the same reason: they describe the state of the
43//! writer's own session, not the shape.
44
45use std::collections::HashMap;
46use std::fmt::Write as _;
47
48use ogeom_core::{OgeomResult, Tolerance, Tolerances, ogeom_bail};
49use ogeom_geom::{
50    BSplineCurve, BSplineSurface, Circle2d, CircleCurve, ConeSurface, Curve, Curve2d as _,
51    Curve3d as _, CylinderSurface, Ellipse2d, EllipseCurve, ExtrusionSurface, HyperbolaCurve,
52    Line2d, LineCurve, ParabolaCurve, PlanarCurve, PlaneSurface, RevolutionSurface, SphereSurface,
53    Surface as _, SurfaceGeometry, TorusSurface, TrimmedCurve, TrimmedSurface,
54};
55use ogeom_math::{
56    Axis, Axis2, Circle, Circle2, Cone, ControlGrid, Cylinder, Direction, Direction2, Ellipse,
57    Ellipse2, Frame, Frame2, Hyperbola, KnotVector, Matrix3, Parabola, Plane, Point, Point2,
58    Sphere, Torus, Transform, Vector, Vector2, Weighted,
59};
60use ogeom_topo::{
61    CurveId, EdgeData, EdgeRepr, FaceData, Location, Model, NodeData, Orientation, PCurveId, Shape,
62    ShapeType, SurfaceId, TShapeId, VertexData,
63};
64
65/// What the format puts at the top of every file, and what a reader looks
66/// for to know it is one. Data, not prose: these bytes are the format.
67const CONTENT_TYPE: &str = "DBRep_DrawableShape";
68const VERSION: &str = "CASCADE Topology V1, (c) Matra-Datavision";
69
70// --- writing -----------------------------------------------------------------
71
72/// Write a shape as interchange text.
73///
74/// # Errors
75///
76/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the
77/// shape carries geometry the format has no record for (a helical curve, a
78/// curve living on a surface, the sinusoidal pcurve an oblique section
79/// leaves on a cylinder), each refused by name rather than approximated into
80/// something the file would claim was exact.
81pub fn write(model: &Model, root: &Shape, tol: Tolerances) -> OgeomResult<String> {
82    let mut tables = Tables::default();
83    tables.gather(model, root)?;
84
85    let mut out = String::new();
86    let _ = writeln!(out, "{CONTENT_TYPE}\n");
87    let _ = writeln!(out, "{VERSION}");
88
89    // Placements. Every location is written as its own matrix rather than as
90    // a composition of others: the composition form exists to let a file
91    // share a chain between many shapes, and nothing here is repeated often
92    // enough for that to pay.
93    let _ = writeln!(out, "Locations {}", tables.locations.len());
94    for location in &tables.locations {
95        let transform = location.composed(model.datums())?;
96        let linear = transform.linear();
97        let scale = transform.scale_factor();
98        let translation = transform.translation_vector();
99        for row in 0..3 {
100            let mut line = String::new();
101            for column in 0..3 {
102                let _ = write!(line, " {}", real(linear.get(row, column)? * scale));
103            }
104            let _ = write!(
105                line,
106                " {}",
107                real(match row {
108                    0 => translation.x,
109                    1 => translation.y,
110                    _ => translation.z,
111                })
112            );
113            let _ = writeln!(out, "1{line}");
114        }
115    }
116
117    let _ = writeln!(out, "Curve2ds {}", tables.pcurves.len());
118    for id in &tables.pcurves {
119        let Some(geometry) = model.geometry().pcurve(*id) else {
120            ogeom_bail!(Dangling, "a pcurve is not in this model");
121        };
122        write_pcurve(&mut out, geometry)?;
123    }
124
125    let _ = writeln!(out, "Curves {}", tables.curves.len());
126    for id in &tables.curves {
127        let Some(geometry) = model.geometry().curve(*id) else {
128            ogeom_bail!(Dangling, "a curve is not in this model");
129        };
130        write_curve(&mut out, geometry)?;
131    }
132
133    // The two cached-mesh sections, empty. They are part of the file's
134    // shape whether or not anything is in them.
135    let _ = writeln!(out, "Polygon3D 0");
136    let _ = writeln!(out, "PolygonOnTriangulations 0");
137
138    let _ = writeln!(out, "Surfaces {}", tables.surfaces.len());
139    for id in &tables.surfaces {
140        let Some(geometry) = model.geometry().surface(*id) else {
141            ogeom_bail!(Dangling, "a surface is not in this model");
142        };
143        write_surface(&mut out, geometry)?;
144    }
145    let _ = writeln!(out, "Triangulations 0");
146
147    // Topology, leaves first. Record numbering counts backwards from the
148    // last, so a child written earlier has the larger number.
149    let total = tables.order.len();
150    let _ = writeln!(out, "\nTShapes {total}");
151    let number_of = |node: TShapeId| -> OgeomResult<usize> {
152        let Some(position) = tables.index_of.get(&node) else {
153            ogeom_bail!(Construction, "a subshape was never written");
154        };
155        Ok(total - position)
156    };
157
158    for node_id in &tables.order {
159        let shape = Shape::of(*node_id);
160        let Some(node) = model.node(&shape) else {
161            ogeom_bail!(Dangling, "a shape is not in this model");
162        };
163        write_record(&mut out, model, &shape, &tables, tol)?;
164
165        // Flags, then the children. Seven flags the format asks for and this
166        // kernel does not keep: they record the writing session's own
167        // bookkeeping (whether a shape was visited, modified, checked), and
168        // reading them back as geometry would be reading somebody else's
169        // scratch paper. Orientable is the one that is always true here.
170        let _ = writeln!(out, "\n0001000");
171        let mut line = String::new();
172        for child in node.children() {
173            let _ = write!(
174                line,
175                "{}{} {} ",
176                match child.orientation() {
177                    Orientation::Forward => "+",
178                    Orientation::Reversed => "-",
179                    Orientation::Internal => "i",
180                    Orientation::External => "e",
181                },
182                number_of(child.node())?,
183                tables.location_number(child.location()),
184            );
185        }
186        let _ = writeln!(out, "{line}*");
187    }
188
189    // The final record says how the whole model is oriented and placed.
190    let _ = writeln!(
191        out,
192        "\n{}{} {}",
193        match root.orientation() {
194            Orientation::Forward => "+",
195            Orientation::Reversed => "-",
196            Orientation::Internal => "i",
197            Orientation::External => "e",
198        },
199        number_of(root.node())?,
200        tables.location_number(root.location()),
201    );
202    Ok(out)
203}
204
205/// The tables a file's sections are: geometry by the order it is written,
206/// topology leaves first, and every placement anything refers to.
207#[derive(Default)]
208struct Tables {
209    curves: Vec<CurveId>,
210    pcurves: Vec<PCurveId>,
211    surfaces: Vec<SurfaceId>,
212    locations: Vec<Location>,
213    order: Vec<TShapeId>,
214    index_of: HashMap<TShapeId, usize>,
215    curve_at: HashMap<CurveId, usize>,
216    pcurve_at: HashMap<PCurveId, usize>,
217    surface_at: HashMap<SurfaceId, usize>,
218    location_at: HashMap<Location, usize>,
219}
220
221impl Tables {
222    /// Walk the shape leaves-first, numbering everything it refers to.
223    fn gather(&mut self, model: &Model, root: &Shape) -> OgeomResult<()> {
224        self.visit(model, root)
225    }
226
227    fn visit(&mut self, model: &Model, shape: &Shape) -> OgeomResult<()> {
228        if self.index_of.contains_key(&shape.node()) {
229            return Ok(());
230        }
231        let Some(node) = model.node(shape) else {
232            ogeom_bail!(Dangling, "shape is not in this model");
233        };
234        // Children first, so a parent's record can name them by how far
235        // above it they were written.
236        for child in node.children() {
237            self.visit(model, child)?;
238            self.take_location(child.location());
239        }
240        match node.data() {
241            NodeData::Edge(data) => {
242                for representation in &data.representations {
243                    match representation {
244                        EdgeRepr::Curve3d { curve, .. } => self.take_curve(*curve),
245                        EdgeRepr::PCurve {
246                            curve,
247                            surface,
248                            location,
249                            ..
250                        } => {
251                            self.take_pcurve(*curve);
252                            self.take_surface(*surface);
253                            self.take_location(location);
254                        }
255                        EdgeRepr::Seam {
256                            forward,
257                            reversed,
258                            surface,
259                            location,
260                            ..
261                        } => {
262                            self.take_pcurve(*forward);
263                            self.take_pcurve(*reversed);
264                            self.take_surface(*surface);
265                            self.take_location(location);
266                        }
267                        _ => {}
268                    }
269                }
270            }
271            NodeData::Face(data) => {
272                self.take_surface(data.surface);
273                self.take_location(&data.location);
274            }
275            NodeData::Vertex(_) | NodeData::Container => {}
276        }
277        self.index_of.insert(shape.node(), self.order.len());
278        self.order.push(shape.node());
279        Ok(())
280    }
281
282    fn take_curve(&mut self, id: CurveId) {
283        if !self.curve_at.contains_key(&id) {
284            self.curve_at.insert(id, self.curves.len());
285            self.curves.push(id);
286        }
287    }
288
289    fn take_pcurve(&mut self, id: PCurveId) {
290        if !self.pcurve_at.contains_key(&id) {
291            self.pcurve_at.insert(id, self.pcurves.len());
292            self.pcurves.push(id);
293        }
294    }
295
296    fn take_surface(&mut self, id: SurfaceId) {
297        if !self.surface_at.contains_key(&id) {
298            self.surface_at.insert(id, self.surfaces.len());
299            self.surfaces.push(id);
300        }
301    }
302
303    fn take_location(&mut self, location: &Location) {
304        if location.is_identity() || self.location_at.contains_key(location) {
305            return;
306        }
307        self.location_at
308            .insert(location.clone(), self.locations.len());
309        self.locations.push(location.clone());
310    }
311
312    /// The file's number for a placement: one-based, and zero for identity,
313    /// which the format spells as "no placement at all".
314    fn location_number(&self, location: &Location) -> usize {
315        if location.is_identity() {
316            return 0;
317        }
318        self.location_at.get(location).map_or(0, |at| at + 1)
319    }
320
321    fn curve_number(&self, id: CurveId) -> usize {
322        self.curve_at.get(&id).map_or(0, |at| at + 1)
323    }
324
325    fn pcurve_number(&self, id: PCurveId) -> usize {
326        self.pcurve_at.get(&id).map_or(0, |at| at + 1)
327    }
328
329    fn surface_number(&self, id: SurfaceId) -> usize {
330        self.surface_at.get(&id).map_or(0, |at| at + 1)
331    }
332}
333
334/// One topology record's own data: everything before the flag word.
335fn write_record(
336    out: &mut String,
337    model: &Model,
338    shape: &Shape,
339    tables: &Tables,
340    tol: Tolerances,
341) -> OgeomResult<()> {
342    let Some(node) = model.node(shape) else {
343        ogeom_bail!(Dangling, "a shape is not in this model");
344    };
345    match node.data() {
346        NodeData::Vertex(data) => {
347            let _ = writeln!(out, "Ve");
348            let _ = writeln!(out, "{}", real(data.tolerance.get()));
349            let _ = writeln!(
350                out,
351                "{} {} {}",
352                real(data.point.x),
353                real(data.point.y),
354                real(data.point.z)
355            );
356            // A vertex's parameters on the curves through it are recoverable
357            // by projection and are not written, so the list is empty.
358            let _ = writeln!(out, "0 0");
359        }
360        NodeData::Edge(data) => {
361            let _ = writeln!(out, "Ed");
362            let _ = writeln!(
363                out,
364                " {} {} {} {}",
365                real(data.tolerance.get()),
366                usize::from(data.same_parameter()),
367                1,
368                usize::from(data.degenerate)
369            );
370            for representation in &data.representations {
371                match representation {
372                    EdgeRepr::Curve3d { curve, range, .. } => {
373                        let _ = writeln!(
374                            out,
375                            "1 {} 0 {} {}",
376                            tables.curve_number(*curve),
377                            real(range.0),
378                            real(range.1)
379                        );
380                    }
381                    EdgeRepr::PCurve {
382                        curve,
383                        range,
384                        surface,
385                        location,
386                    } => {
387                        let _ = writeln!(
388                            out,
389                            "2 {} {} {} {} {}",
390                            tables.pcurve_number(*curve),
391                            tables.surface_number(*surface),
392                            tables.location_number(location),
393                            real(range.0),
394                            real(range.1)
395                        );
396                    }
397                    EdgeRepr::Seam {
398                        forward,
399                        reversed,
400                        range,
401                        surface,
402                        location,
403                    } => {
404                        let _ = writeln!(
405                            out,
406                            "3 {} {} C0 {} {} {} {}",
407                            tables.pcurve_number(*forward),
408                            tables.pcurve_number(*reversed),
409                            tables.surface_number(*surface),
410                            tables.location_number(location),
411                            real(range.0),
412                            real(range.1)
413                        );
414                    }
415                    other => ogeom_bail!(
416                        Construction,
417                        "an edge carries a representation this format has no \
418                         record for: {other:?}"
419                    ),
420                }
421            }
422            let _ = writeln!(out, "0");
423        }
424        NodeData::Face(data) => {
425            let _ = writeln!(out, "Fa");
426            let _ = writeln!(
427                out,
428                "0 {} {} {}",
429                real(data.tolerance.get()),
430                tables.surface_number(data.surface),
431                tables.location_number(&data.location)
432            );
433        }
434        NodeData::Container => {
435            let kind = match model.kind_of(shape)? {
436                ShapeType::Wire => "Wi",
437                ShapeType::Shell => "Sh",
438                ShapeType::Solid => "So",
439                ShapeType::CompSolid => "CS",
440                ShapeType::Compound => "Co",
441                other => ogeom_bail!(Construction, "a {other:?} has no record in this format"),
442            };
443            let _ = writeln!(out, "{kind}\n");
444        }
445    }
446    let _ = tol;
447    Ok(())
448}
449
450fn write_curve(out: &mut String, curve: &Curve) -> OgeomResult<()> {
451    match curve {
452        Curve::Line(l) => {
453            let axis = l.axis();
454            let _ = writeln!(
455                out,
456                "1 {} {}",
457                point(axis.location),
458                direction(axis.direction.vector())
459            );
460        }
461        Curve::Circle(c) => {
462            let circle = c.circle();
463            let frame = circle.frame();
464            let _ = writeln!(
465                out,
466                "2 {} {} {} {} {}",
467                point(frame.origin()),
468                direction(frame.z().vector()),
469                direction(frame.x().vector()),
470                direction(frame.y().vector()),
471                real(circle.radius())
472            );
473        }
474        Curve::Ellipse(e) => {
475            let ellipse = e.ellipse();
476            let frame = ellipse.frame();
477            let _ = writeln!(
478                out,
479                "3 {} {} {} {} {} {}",
480                point(frame.origin()),
481                direction(frame.z().vector()),
482                direction(frame.x().vector()),
483                direction(frame.y().vector()),
484                real(ellipse.major_radius()),
485                real(ellipse.minor_radius())
486            );
487        }
488        Curve::Parabola(p) => {
489            let parabola = p.parabola();
490            let frame = parabola.frame();
491            let _ = writeln!(
492                out,
493                "4 {} {} {} {} {}",
494                point(frame.origin()),
495                direction(frame.z().vector()),
496                direction(frame.x().vector()),
497                direction(frame.y().vector()),
498                real(parabola.focal())
499            );
500        }
501        Curve::Hyperbola(h) => {
502            let hyperbola = h.hyperbola();
503            let frame = hyperbola.frame();
504            let _ = writeln!(
505                out,
506                "5 {} {} {} {} {} {}",
507                point(frame.origin()),
508                direction(frame.z().vector()),
509                direction(frame.x().vector()),
510                direction(frame.y().vector()),
511                real(hyperbola.major_radius()),
512                real(hyperbola.minor_radius())
513            );
514        }
515        Curve::BSpline(b) => {
516            let rational = b
517                .control_points()
518                .iter()
519                .any(|c| (c.weight - 1.0).abs() > 0.0);
520            let distinct = b.knots().distinct();
521            let mut line = format!(
522                "7 {} 0  {} {} {}",
523                usize::from(rational),
524                b.knots().degree(),
525                b.control_points().len(),
526                distinct.len()
527            );
528            for control in b.control_points() {
529                let at = control.point();
530                let _ = write!(line, " {}", point(at));
531                if rational {
532                    let _ = write!(line, " {}", real(control.weight));
533                }
534            }
535            let _ = writeln!(out, "{line}");
536            let mut knot_line = String::new();
537            for (value, multiplicity) in distinct {
538                let _ = write!(knot_line, " {} {multiplicity}", real(value));
539            }
540            let _ = writeln!(out, "{knot_line}");
541        }
542        Curve::Trimmed(t) => {
543            let (lo, hi) = t.domain();
544            let _ = writeln!(out, "8 {} {}", real(lo), real(hi));
545            write_curve(out, t.basis())?;
546        }
547        Curve::Offset(o) => {
548            let _ = writeln!(
549                out,
550                "9 {} {}",
551                real(o.distance()),
552                direction(o.reference().vector())
553            );
554            write_curve(out, o.basis())?;
555        }
556        Curve::Helix(_) | Curve::OnSurface(_) => ogeom_bail!(
557            Construction,
558            "the format has no record for a helix or a curve carried on a \
559             surface, and writing one as anything else would be writing a \
560             different curve"
561        ),
562    }
563    Ok(())
564}
565
566fn write_pcurve(out: &mut String, curve: &PlanarCurve) -> OgeomResult<()> {
567    match curve {
568        PlanarCurve::Line(l) => {
569            let axis = l.axis();
570            let _ = writeln!(
571                out,
572                "1 {} {}",
573                point2(axis.location),
574                direction2(axis.direction.vector())
575            );
576        }
577        PlanarCurve::Circle(c) => {
578            let circle = c.circle();
579            let frame = circle.frame();
580            let _ = writeln!(
581                out,
582                "2 {} {} {} {}",
583                point2(frame.origin()),
584                direction2(frame.x().vector()),
585                direction2(frame.y().vector()),
586                real(circle.radius())
587            );
588        }
589        PlanarCurve::Ellipse(e) => {
590            let ellipse = e.ellipse();
591            let frame = ellipse.frame();
592            let _ = writeln!(
593                out,
594                "3 {} {} {} {} {}",
595                point2(frame.origin()),
596                direction2(frame.x().vector()),
597                direction2(frame.y().vector()),
598                real(ellipse.major_radius()),
599                real(ellipse.minor_radius())
600            );
601        }
602        PlanarCurve::BSpline(b) => {
603            let rational = b
604                .control_points()
605                .iter()
606                .any(|c| (c.weight - 1.0).abs() > 0.0);
607            let distinct = b.knots().distinct();
608            let mut line = format!(
609                "7 {} 0  {} {} {}",
610                usize::from(rational),
611                b.knots().degree(),
612                b.control_points().len(),
613                distinct.len()
614            );
615            for control in b.control_points() {
616                let at = control.point();
617                let _ = write!(line, " {}", point2(at));
618                if rational {
619                    let _ = write!(line, " {}", real(control.weight));
620                }
621            }
622            let _ = writeln!(out, "{line}");
623            let mut knot_line = String::new();
624            for (value, multiplicity) in distinct {
625                let _ = write!(knot_line, " {} {multiplicity}", real(value));
626            }
627            let _ = writeln!(out, "{knot_line}");
628        }
629        PlanarCurve::Trimmed(t) => {
630            let (lo, hi) = t.domain();
631            let _ = writeln!(out, "8 {} {}", real(lo), real(hi));
632            write_pcurve(out, t.basis())?;
633        }
634        PlanarCurve::Offset(o) => {
635            let _ = writeln!(out, "9 {}", real(o.distance()));
636            write_pcurve(out, o.basis())?;
637        }
638        PlanarCurve::Trig(_) => ogeom_bail!(
639            Construction,
640            "the format has no record for the sinusoidal chart curve an \
641             oblique section leaves on a cylinder; writing it as a spline \
642             would be writing a fit and calling it exact"
643        ),
644    }
645    Ok(())
646}
647
648fn write_surface(out: &mut String, surface: &SurfaceGeometry) -> OgeomResult<()> {
649    // The elementary surfaces all begin the same way: an origin and the
650    // frame's three directions, normal first.
651    let seat = |frame: &Frame| {
652        format!(
653            "{} {} {} {}",
654            point(frame.origin()),
655            direction(frame.z().vector()),
656            direction(frame.x().vector()),
657            direction(frame.y().vector())
658        )
659    };
660    match surface {
661        SurfaceGeometry::Plane(p) => {
662            let _ = writeln!(out, "1 {}", seat(&p.plane().frame()));
663        }
664        SurfaceGeometry::Cylinder(c) => {
665            let _ = writeln!(
666                out,
667                "2 {} {}",
668                seat(&c.cylinder().frame()),
669                real(c.cylinder().radius())
670            );
671        }
672        SurfaceGeometry::Cone(c) => {
673            let cone = c.cone();
674            let _ = writeln!(
675                out,
676                "3 {} {} {}",
677                seat(&cone.frame()),
678                real(cone.reference_radius()),
679                real(cone.half_angle())
680            );
681        }
682        SurfaceGeometry::Sphere(s) => {
683            let _ = writeln!(
684                out,
685                "4 {} {}",
686                seat(&s.sphere().frame()),
687                real(s.sphere().radius())
688            );
689        }
690        SurfaceGeometry::Torus(t) => {
691            let torus = t.torus();
692            let _ = writeln!(
693                out,
694                "5 {} {} {}",
695                seat(&torus.frame()),
696                real(torus.major_radius()),
697                real(torus.minor_radius())
698            );
699        }
700        SurfaceGeometry::Extrusion(e) => {
701            let _ = writeln!(out, "6 {}", direction(e.direction().vector()));
702            write_curve(out, e.curve())?;
703        }
704        SurfaceGeometry::Revolution(r) => {
705            let axis = r.axis();
706            let _ = writeln!(
707                out,
708                "7 {} {}",
709                point(axis.location),
710                direction(axis.direction.vector())
711            );
712            write_curve(out, r.curve())?;
713        }
714        SurfaceGeometry::BSpline(b) => {
715            let grid = b.grid();
716            let rational = grid.points().iter().any(|c| (c.weight - 1.0).abs() > 0.0);
717            let u_distinct = b.u_knots().distinct();
718            let v_distinct = b.v_knots().distinct();
719            let _ = writeln!(
720                out,
721                "9 {} {} 0 0 {} {} {} {} {} {}",
722                usize::from(rational),
723                usize::from(rational),
724                b.u_knots().degree(),
725                b.v_knots().degree(),
726                grid.u_count(),
727                grid.v_count(),
728                u_distinct.len(),
729                v_distinct.len()
730            );
731            // Poles run u-major: all of one u row's v values, then the next.
732            for i in 0..grid.u_count() {
733                let mut line = String::new();
734                for j in 0..grid.v_count() {
735                    let control = grid.points()[i * grid.v_count() + j];
736                    let at = control.point();
737                    let _ = write!(line, " {}", point(at));
738                    if rational {
739                        let _ = write!(line, " {}", real(control.weight));
740                    }
741                }
742                let _ = writeln!(out, "{line}");
743            }
744            let mut line = String::new();
745            for (value, multiplicity) in u_distinct {
746                let _ = write!(line, " {} {multiplicity}", real(value));
747            }
748            let _ = writeln!(out, "{line}");
749            let mut line = String::new();
750            for (value, multiplicity) in v_distinct {
751                let _ = write!(line, " {} {multiplicity}", real(value));
752            }
753            let _ = writeln!(out, "{line}");
754        }
755        SurfaceGeometry::Trimmed(t) => {
756            let ((u0, u1), (v0, v1)) = t.domain();
757            let _ = writeln!(
758                out,
759                "10 {} {} {} {}",
760                real(u0),
761                real(u1),
762                real(v0),
763                real(v1)
764            );
765            write_surface(out, t.basis())?;
766        }
767        SurfaceGeometry::Offset(o) => {
768            let _ = writeln!(out, "11 {}", real(o.distance()));
769            write_surface(out, o.basis())?;
770        }
771    }
772    Ok(())
773}
774
775/// A number, written so it reads back as itself.
776fn real(v: f64) -> String {
777    let mut text = format!("{v:?}");
778    if text.ends_with(".0") {
779        text.truncate(text.len() - 2);
780    }
781    text
782}
783
784fn point(p: Point) -> String {
785    format!("{} {} {}", real(p.x), real(p.y), real(p.z))
786}
787
788fn point2(p: Point2) -> String {
789    format!("{} {}", real(p.x), real(p.y))
790}
791
792fn direction(v: Vector) -> String {
793    format!("{} {} {}", real(v.x), real(v.y), real(v.z))
794}
795
796fn direction2(v: Vector2) -> String {
797    format!("{} {}", real(v.x), real(v.y))
798}
799
800// --- reading -----------------------------------------------------------------
801
802/// Read a shape from interchange text, into a model of its own.
803///
804/// # Errors
805///
806/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the
807/// text is not this format, or carries a record this does not read, which
808/// it says by number rather than by shrugging.
809pub fn read(text: &str, tol: Tolerances) -> OgeomResult<(Model, Shape)> {
810    let mut lines = text.lines();
811    let Some(first) = lines.next() else {
812        ogeom_bail!(Construction, "the file is empty");
813    };
814    if first.trim() != CONTENT_TYPE {
815        ogeom_bail!(
816            Construction,
817            "this is not the interchange format: it opens with `{}`",
818            first.trim()
819        );
820    }
821    // The version line is whatever the writer put there; every version this
822    // reads differs only in sections this skips.
823    let rest: String = lines.collect::<Vec<_>>().join("\n");
824    let Some(at) = rest.find("Locations") else {
825        ogeom_bail!(Construction, "the file has no placement table");
826    };
827    let mut cursor = Cursor::new(&rest[at..]);
828    let mut model = Model::new();
829    let mut built = Built::default();
830
831    cursor.expect("Locations")?;
832    let count = cursor.count()?;
833    for _ in 0..count {
834        let kind = cursor.count()?;
835        match kind {
836            1 => {
837                let mut rows = [[0.0_f64; 3]; 3];
838                let mut translation = [0.0_f64; 3];
839                for (row, offset) in rows.iter_mut().zip(&mut translation) {
840                    for cell in row.iter_mut() {
841                        *cell = cursor.number()?;
842                    }
843                    *offset = cursor.number()?;
844                }
845                built.locations.push(transform_of(rows, translation, tol)?);
846            }
847            2 => {
848                // A composition of placements already read, each raised to a
849                // power, ending at a zero.
850                let mut composed = Transform::IDENTITY;
851                loop {
852                    let index = cursor.count()?;
853                    if index == 0 {
854                        break;
855                    }
856                    let power: i64 = cursor.integer()?;
857                    let Some(base) = built.locations.get(index - 1).copied() else {
858                        ogeom_bail!(
859                            Construction,
860                            "a composed placement names placement {index}, which is not above it"
861                        );
862                    };
863                    let step = if power < 0 { base.inverse()? } else { base };
864                    for _ in 0..power.abs() {
865                        composed = composed * step;
866                    }
867                }
868                built.locations.push(composed);
869            }
870            other => ogeom_bail!(
871                Construction,
872                "placement record {other} is not one this reads"
873            ),
874        }
875    }
876
877    cursor.expect("Curve2ds")?;
878    let count = cursor.count()?;
879    for _ in 0..count {
880        let curve = cursor.pcurve(tol)?;
881        built.pcurves.push(model.geometry_mut().add_pcurve(curve));
882    }
883
884    cursor.expect("Curves")?;
885    let count = cursor.count()?;
886    for _ in 0..count {
887        let curve = cursor.curve(tol)?;
888        built.curves.push(model.geometry_mut().add_curve(curve));
889    }
890
891    // The cached-mesh sections, counted so they can be stepped over.
892    cursor.expect("Polygon3D")?;
893    let count = cursor.count()?;
894    for _ in 0..count {
895        cursor.skip_polygon3d()?;
896    }
897    cursor.expect("PolygonOnTriangulations")?;
898    let count = cursor.count()?;
899    for _ in 0..count {
900        cursor.skip_polygon_on_triangulation()?;
901    }
902
903    cursor.expect("Surfaces")?;
904    let count = cursor.count()?;
905    for _ in 0..count {
906        let surface = cursor.surface(tol)?;
907        built
908            .surfaces
909            .push(model.geometry_mut().add_surface(surface));
910    }
911
912    cursor.expect("Triangulations")?;
913    let count = cursor.count()?;
914    for _ in 0..count {
915        cursor.skip_triangulation()?;
916    }
917
918    cursor.expect("TShapes")?;
919    let total = cursor.count()?;
920    // Records are read in file order and numbered backwards, so the record
921    // just read is number `total - written`, and every child it names has
922    // already been built.
923    let mut shapes: Vec<Shape> = Vec::with_capacity(total);
924    for _ in 0..total {
925        let shape = cursor.record(&mut model, &built, &shapes, total, tol)?;
926        shapes.push(shape);
927    }
928    // The last reference is the file's own shape: how the whole model is
929    // oriented and placed.
930    let (orientation, number, location) = cursor.reference()?;
931    let Some(root) = pick(&shapes, number, total) else {
932        ogeom_bail!(
933            Construction,
934            "the file's own shape is numbered {number}, which is not there"
935        );
936    };
937    let root = placed(root, &built, location, &mut model)?.composed(orientation);
938    Ok((model, root))
939}
940
941/// How far an unbounded conic runs when it is read.
942///
943/// The format's parabola and hyperbola records carry a focus and a frame and
944/// no parameter window; the window is the *edge's* business, and every edge
945/// states its own range. So the curve is built over a window wide enough for
946/// any edge a file of ordinary size could hold, and the edge trims it.
947const OPEN_EXTENT: f64 = 1e6;
948
949/// Everything read so far that later records refer to by number.
950#[derive(Default)]
951struct Built {
952    locations: Vec<Transform>,
953    curves: Vec<CurveId>,
954    pcurves: Vec<PCurveId>,
955    surfaces: Vec<SurfaceId>,
956}
957
958/// The shape a backward record number names.
959fn pick(shapes: &[Shape], number: usize, total: usize) -> Option<Shape> {
960    // Record number `n` counts back from the last: it is the one at position
961    // `total - n` in file order.
962    total
963        .checked_sub(number)
964        .and_then(|at| shapes.get(at))
965        .cloned()
966}
967
968/// A transform from the format's three rows and translation column.
969fn transform_of(
970    rows: [[f64; 3]; 3],
971    translation: [f64; 3],
972    tol: Tolerances,
973) -> OgeomResult<Transform> {
974    // The three-by-three carries the scale inside it: its determinant is the
975    // cube of the uniform factor, and dividing it out leaves the rotation.
976    let m = Matrix3::new(rows);
977    let determinant = m.determinant();
978    if determinant.abs() <= f64::MIN_POSITIVE {
979        ogeom_bail!(
980            Construction,
981            "a placement with a singular matrix places nothing"
982        );
983    }
984    let scale = determinant.cbrt();
985    let mut unit = [[0.0_f64; 3]; 3];
986    for row in 0..3 {
987        for column in 0..3 {
988            unit[row][column] = rows[row][column] / scale;
989        }
990    }
991    Transform::from_parts(
992        Matrix3::new(unit),
993        scale,
994        Vector::new(translation[0], translation[1], translation[2]),
995        tol.angular().max(1e-9),
996    )
997}
998
999/// Place a shape by the file's placement number.
1000fn placed(shape: Shape, built: &Built, number: usize, model: &mut Model) -> OgeomResult<Shape> {
1001    if number == 0 {
1002        return Ok(shape);
1003    }
1004    let Some(transform) = built.locations.get(number - 1) else {
1005        ogeom_bail!(
1006            Construction,
1007            "a shape names placement {number}, which is not there"
1008        );
1009    };
1010    let datum = model.add_datum(*transform);
1011    Ok(shape.moved(&Location::of(datum)))
1012}
1013
1014/// A whitespace-delimited walk over the file's body.
1015struct Cursor<'a> {
1016    tokens: Vec<&'a str>,
1017    at: usize,
1018}
1019
1020impl<'a> Cursor<'a> {
1021    fn new(text: &'a str) -> Self {
1022        Self {
1023            tokens: text.split_whitespace().collect(),
1024            at: 0,
1025        }
1026    }
1027
1028    fn word(&mut self) -> OgeomResult<&'a str> {
1029        let Some(token) = self.tokens.get(self.at) else {
1030            ogeom_bail!(Construction, "the file ends in the middle of a record");
1031        };
1032        self.at += 1;
1033        Ok(token)
1034    }
1035
1036    fn peek(&self) -> Option<&'a str> {
1037        self.tokens.get(self.at).copied()
1038    }
1039
1040    fn expect(&mut self, what: &str) -> OgeomResult<()> {
1041        let token = self.word()?;
1042        if token != what {
1043            ogeom_bail!(Construction, "expected the {what} table, found `{token}`");
1044        }
1045        Ok(())
1046    }
1047
1048    fn number(&mut self) -> OgeomResult<f64> {
1049        let token = self.word()?;
1050        let Ok(value) = token.parse::<f64>() else {
1051            ogeom_bail!(Construction, "`{token}` is not a number");
1052        };
1053        Ok(value)
1054    }
1055
1056    fn integer(&mut self) -> OgeomResult<i64> {
1057        let token = self.word()?;
1058        let Ok(value) = token.parse::<i64>() else {
1059            ogeom_bail!(Construction, "`{token}` is not a whole number");
1060        };
1061        Ok(value)
1062    }
1063
1064    fn count(&mut self) -> OgeomResult<usize> {
1065        let value = self.integer()?;
1066        let Ok(count) = usize::try_from(value) else {
1067            ogeom_bail!(Construction, "{value} is not a count");
1068        };
1069        Ok(count)
1070    }
1071
1072    fn flag(&mut self) -> OgeomResult<bool> {
1073        Ok(self.count()? != 0)
1074    }
1075
1076    fn point(&mut self) -> OgeomResult<Point> {
1077        Ok(Point::new(self.number()?, self.number()?, self.number()?))
1078    }
1079
1080    fn point2(&mut self) -> OgeomResult<Point2> {
1081        Ok(Point2::new(self.number()?, self.number()?))
1082    }
1083
1084    fn direction(&mut self, tol: Tolerances) -> OgeomResult<Direction> {
1085        let v = Vector::new(self.number()?, self.number()?, self.number()?);
1086        Direction::new(v, tol)
1087    }
1088
1089    fn direction2(&mut self, tol: Tolerances) -> OgeomResult<Direction2> {
1090        let v = Vector2::new(self.number()?, self.number()?);
1091        Direction2::new(v, tol)
1092    }
1093
1094    /// The origin-normal-x-y seat every elementary record opens with.
1095    fn seat(&mut self, tol: Tolerances) -> OgeomResult<Frame> {
1096        let origin = self.point()?;
1097        let normal = self.direction(tol)?;
1098        let x = self.direction(tol)?;
1099        let _y = self.direction(tol)?;
1100        Frame::new(origin, normal, x, tol)
1101    }
1102
1103    fn seat2(&mut self, tol: Tolerances) -> OgeomResult<Frame2> {
1104        let origin = self.point2()?;
1105        let x = self.direction2(tol)?;
1106        let y = self.direction2(tol)?;
1107        Frame2::from_axes(origin, x, y, tol)
1108    }
1109
1110    /// Poles and knots, as both spline records spell them.
1111    fn spline_knots(&mut self, degree: usize, count: usize) -> OgeomResult<KnotVector> {
1112        let mut flat = Vec::new();
1113        for _ in 0..count {
1114            let value = self.number()?;
1115            let multiplicity = self.count()?;
1116            for _ in 0..multiplicity {
1117                flat.push(value);
1118            }
1119        }
1120        KnotVector::new(flat, degree)
1121    }
1122
1123    fn curve(&mut self, tol: Tolerances) -> OgeomResult<Curve> {
1124        let kind = self.count()?;
1125        Ok(match kind {
1126            1 => {
1127                let origin = self.point()?;
1128                let direction = self.direction(tol)?;
1129                // The record carries no window; the edge's own range is what
1130                // bounds it, so the curve is built wide and trimmed there.
1131                LineCurve::over(Axis::new(origin, direction), -OPEN_EXTENT, OPEN_EXTENT)?.into()
1132            }
1133            2 => {
1134                let frame = self.seat(tol)?;
1135                CircleCurve::new(Circle::new(frame, self.number()?, tol)?).into()
1136            }
1137            3 => {
1138                let frame = self.seat(tol)?;
1139                let ellipse = Ellipse::new(frame, self.number()?, self.number()?, tol)?;
1140                EllipseCurve::new(ellipse).into()
1141            }
1142            4 => {
1143                let frame = self.seat(tol)?;
1144                let parabola = Parabola::new(frame, self.number()?, tol)?;
1145                ParabolaCurve::new(parabola, OPEN_EXTENT)?.into()
1146            }
1147            5 => {
1148                let frame = self.seat(tol)?;
1149                let hyperbola = Hyperbola::new(frame, self.number()?, self.number()?, tol)?;
1150                HyperbolaCurve::new(hyperbola, OPEN_EXTENT)?.into()
1151            }
1152            7 => {
1153                let rational = self.flag()?;
1154                let _periodic = self.flag()?;
1155                let degree = self.count()?;
1156                let poles = self.count()?;
1157                let knot_count = self.count()?;
1158                let mut control = Vec::with_capacity(poles);
1159                for _ in 0..poles {
1160                    let at = self.point()?;
1161                    let weight = if rational { self.number()? } else { 1.0 };
1162                    control.push(Weighted::new(at, weight, tol)?);
1163                }
1164                let knots = self.spline_knots(degree, knot_count)?;
1165                BSplineCurve::rational(knots, control)?.into()
1166            }
1167            8 => {
1168                let lo = self.number()?;
1169                let hi = self.number()?;
1170                let basis = self.curve(tol)?;
1171                Curve::Trimmed(Box::new(TrimmedCurve::new(basis, lo, hi, tol)?))
1172            }
1173            9 => {
1174                let distance = self.number()?;
1175                let reference = self.direction(tol)?;
1176                let basis = self.curve(tol)?;
1177                Curve::Offset(Box::new(ogeom_geom::OffsetCurve::new(
1178                    basis, distance, reference,
1179                )?))
1180            }
1181            other => ogeom_bail!(
1182                Construction,
1183                "curve record {other} is one this does not read; a Bezier \
1184                 curve is record 6, and nothing here builds one"
1185            ),
1186        })
1187    }
1188
1189    fn pcurve(&mut self, tol: Tolerances) -> OgeomResult<PlanarCurve> {
1190        let kind = self.count()?;
1191        Ok(match kind {
1192            1 => {
1193                let origin = self.point2()?;
1194                let direction = self.direction2(tol)?;
1195                Line2d::over(Axis2::new(origin, direction), -OPEN_EXTENT, OPEN_EXTENT)?.into()
1196            }
1197            2 => {
1198                let frame = self.seat2(tol)?;
1199                Circle2d::new(Circle2::new(frame, self.number()?, tol)?).into()
1200            }
1201            3 => {
1202                let frame = self.seat2(tol)?;
1203                let ellipse = Ellipse2::new(frame, self.number()?, self.number()?, tol)?;
1204                Ellipse2d::new(ellipse).into()
1205            }
1206            7 => {
1207                let rational = self.flag()?;
1208                let _periodic = self.flag()?;
1209                let degree = self.count()?;
1210                let poles = self.count()?;
1211                let knot_count = self.count()?;
1212                let mut control = Vec::with_capacity(poles);
1213                for _ in 0..poles {
1214                    let at = self.point2()?;
1215                    let weight = if rational { self.number()? } else { 1.0 };
1216                    control.push(Weighted::new(at, weight, tol)?);
1217                }
1218                let knots = self.spline_knots(degree, knot_count)?;
1219                ogeom_geom::BSpline2d::rational(knots, control)?.into()
1220            }
1221            8 => {
1222                let lo = self.number()?;
1223                let hi = self.number()?;
1224                let basis = self.pcurve(tol)?;
1225                PlanarCurve::Trimmed(Box::new(ogeom_geom::Trimmed2d::new(basis, lo, hi, tol)?))
1226            }
1227            9 => {
1228                let distance = self.number()?;
1229                let basis = self.pcurve(tol)?;
1230                PlanarCurve::Offset(Box::new(ogeom_geom::Offset2d::new(basis, distance)?))
1231            }
1232            other => ogeom_bail!(
1233                Construction,
1234                "chart curve record {other} is one this does not read"
1235            ),
1236        })
1237    }
1238
1239    fn surface(&mut self, tol: Tolerances) -> OgeomResult<SurfaceGeometry> {
1240        let kind = self.count()?;
1241        // An unbounded carrier needs a window to live in; the format gives
1242        // none, so one wide enough for anything the file can hold is used,
1243        // and the face's own trim is what bounds it in practice.
1244        const REACH: (f64, f64) = (-1e9, 1e9);
1245        Ok(match kind {
1246            1 => {
1247                let frame = self.seat(tol)?;
1248                PlaneSurface::over(Plane::new(frame), REACH, REACH)?.into()
1249            }
1250            2 => {
1251                let frame = self.seat(tol)?;
1252                let cylinder = Cylinder::new(frame, self.number()?, tol)?;
1253                CylinderSurface::new(cylinder, REACH)?.into()
1254            }
1255            3 => {
1256                let frame = self.seat(tol)?;
1257                let cone = Cone::new(frame, self.number()?, self.number()?, tol)?;
1258                ConeSurface::new(cone, REACH)?.into()
1259            }
1260            4 => {
1261                let frame = self.seat(tol)?;
1262                SphereSurface::new(Sphere::new(frame, self.number()?, tol)?).into()
1263            }
1264            5 => {
1265                let frame = self.seat(tol)?;
1266                let torus = Torus::new(frame, self.number()?, self.number()?, tol)?;
1267                TorusSurface::new(torus).into()
1268            }
1269            6 => {
1270                let direction = self.direction(tol)?;
1271                let basis = self.curve(tol)?;
1272                ExtrusionSurface::new(basis, direction, 1e9)?.into()
1273            }
1274            7 => {
1275                let origin = self.point()?;
1276                let direction = self.direction(tol)?;
1277                let basis = self.curve(tol)?;
1278                RevolutionSurface::new(basis, Axis::new(origin, direction), core::f64::consts::TAU)?
1279                    .into()
1280            }
1281            9 => {
1282                let u_rational = self.flag()?;
1283                let v_rational = self.flag()?;
1284                let _u_periodic = self.flag()?;
1285                let _v_periodic = self.flag()?;
1286                let u_degree = self.count()?;
1287                let v_degree = self.count()?;
1288                let u_poles = self.count()?;
1289                let v_poles = self.count()?;
1290                let u_knot_count = self.count()?;
1291                let v_knot_count = self.count()?;
1292                let rational = u_rational || v_rational;
1293                let mut points = Vec::with_capacity(u_poles * v_poles);
1294                for _ in 0..u_poles * v_poles {
1295                    let at = self.point()?;
1296                    let weight = if rational { self.number()? } else { 1.0 };
1297                    points.push(Weighted::new(at, weight, tol)?);
1298                }
1299                let u_knots = self.spline_knots(u_degree, u_knot_count)?;
1300                let v_knots = self.spline_knots(v_degree, v_knot_count)?;
1301                let grid = ControlGrid::new(points, u_poles, v_poles)?;
1302                BSplineSurface::rational(u_knots, v_knots, grid)?.into()
1303            }
1304            10 => {
1305                let u = (self.number()?, self.number()?);
1306                let v = (self.number()?, self.number()?);
1307                let basis = self.surface(tol)?;
1308                SurfaceGeometry::Trimmed(Box::new(TrimmedSurface::new(basis, u, v, tol)?))
1309            }
1310            11 => {
1311                let distance = self.number()?;
1312                let basis = self.surface(tol)?;
1313                SurfaceGeometry::Offset(Box::new(ogeom_geom::OffsetSurface::new(basis, distance)?))
1314            }
1315            other => ogeom_bail!(
1316                Construction,
1317                "surface record {other} is one this does not read; a Bezier \
1318                 surface is record 8, and nothing here builds one"
1319            ),
1320        })
1321    }
1322
1323    /// A subshape reference: its orientation, its backward record number and
1324    /// its placement.
1325    fn reference(&mut self) -> OgeomResult<(Orientation, usize, usize)> {
1326        let token = self.word()?;
1327        let (orientation, digits) = match token.split_at(1) {
1328            ("+", rest) => (Orientation::Forward, rest),
1329            ("-", rest) => (Orientation::Reversed, rest),
1330            ("i", rest) => (Orientation::Internal, rest),
1331            ("e", rest) => (Orientation::External, rest),
1332            _ => ogeom_bail!(Construction, "`{token}` does not name a subshape"),
1333        };
1334        let Ok(number) = digits.parse::<usize>() else {
1335            ogeom_bail!(Construction, "`{token}` does not name a subshape number");
1336        };
1337        Ok((orientation, number, self.count()?))
1338    }
1339
1340    fn skip_polygon3d(&mut self) -> OgeomResult<()> {
1341        let nodes = self.count()?;
1342        let has_parameters = self.flag()?;
1343        let _deflection = self.number()?;
1344        for _ in 0..nodes * 3 {
1345            let _ = self.number()?;
1346        }
1347        if has_parameters {
1348            for _ in 0..nodes {
1349                let _ = self.number()?;
1350            }
1351        }
1352        Ok(())
1353    }
1354
1355    fn skip_polygon_on_triangulation(&mut self) -> OgeomResult<()> {
1356        let nodes = self.count()?;
1357        for _ in 0..nodes {
1358            let _ = self.integer()?;
1359        }
1360        // An optional parameter list follows, introduced by `p`.
1361        if self.peek() == Some("p") {
1362            let _ = self.word()?;
1363            let _deflection = self.number()?;
1364            let _flag = self.count()?;
1365            let count = self.count()?;
1366            for _ in 0..count {
1367                let _ = self.number()?;
1368            }
1369        }
1370        Ok(())
1371    }
1372
1373    fn skip_triangulation(&mut self) -> OgeomResult<()> {
1374        let nodes = self.count()?;
1375        let triangles = self.count()?;
1376        let has_parameters = self.flag()?;
1377        let _deflection = self.number()?;
1378        for _ in 0..nodes * 3 {
1379            let _ = self.number()?;
1380        }
1381        if has_parameters {
1382            for _ in 0..nodes * 2 {
1383                let _ = self.number()?;
1384            }
1385        }
1386        for _ in 0..triangles * 3 {
1387            let _ = self.integer()?;
1388        }
1389        Ok(())
1390    }
1391
1392    /// One topology record, built into the model against what came before.
1393    fn record(
1394        &mut self,
1395        model: &mut Model,
1396        built: &Built,
1397        shapes: &[Shape],
1398        total: usize,
1399        tol: Tolerances,
1400    ) -> OgeomResult<Shape> {
1401        let kind = self.word()?;
1402        let data = match kind {
1403            "Ve" => {
1404                let tolerance = self.number()?;
1405                let at = self.point()?;
1406                // The parameter list, which this recovers by projection where
1407                // it needs it rather than trusting a file's copy.
1408                loop {
1409                    let first = self.word()?;
1410                    let second = self.word()?;
1411                    if first == "0" && second == "0" {
1412                        break;
1413                    }
1414                    // A representation: its data depends on the kind in
1415                    // `second`, and every one ends with a placement number.
1416                    match second {
1417                        "1" => {
1418                            let _ = self.count()?;
1419                        }
1420                        "2" | "3" => {
1421                            let _ = self.count()?;
1422                            let _ = self.count()?;
1423                        }
1424                        other => ogeom_bail!(
1425                            Construction,
1426                            "a vertex representation of kind {other} is not one this reads"
1427                        ),
1428                    }
1429                    let _ = self.count()?;
1430                }
1431                Record::Vertex(VertexData {
1432                    point: at,
1433                    tolerance: Tolerance::new(tolerance.max(tol.confusion()))?,
1434                })
1435            }
1436            "Ed" => {
1437                let tolerance = self.number()?;
1438                let _same_parameter = self.flag()?;
1439                let _same_range = self.flag()?;
1440                let degenerate = self.flag()?;
1441                let mut data = EdgeData::new();
1442                data.tolerance = Tolerance::new(tolerance.max(tol.confusion()))?;
1443                data.degenerate = degenerate;
1444                loop {
1445                    let kind = self.count()?;
1446                    match kind {
1447                        0 => break,
1448                        1 => {
1449                            let curve = self.count()?;
1450                            let _location = self.count()?;
1451                            let lo = self.number()?;
1452                            let hi = self.number()?;
1453                            let Some(id) = built.curves.get(curve - 1).copied() else {
1454                                ogeom_bail!(Construction, "an edge names curve {curve}");
1455                            };
1456                            data.add(EdgeRepr::Curve3d {
1457                                curve: id,
1458                                range: (lo, hi),
1459                                location: Location::identity(),
1460                            });
1461                        }
1462                        2 => {
1463                            let pcurve = self.count()?;
1464                            let surface = self.count()?;
1465                            let _location = self.count()?;
1466                            let lo = self.number()?;
1467                            let hi = self.number()?;
1468                            let (Some(curve), Some(on)) = (
1469                                built.pcurves.get(pcurve - 1).copied(),
1470                                built.surfaces.get(surface - 1).copied(),
1471                            ) else {
1472                                ogeom_bail!(
1473                                    Construction,
1474                                    "an edge names chart curve {pcurve} on surface {surface}"
1475                                );
1476                            };
1477                            data.add(EdgeRepr::PCurve {
1478                                curve,
1479                                range: (lo, hi),
1480                                surface: on,
1481                                location: Location::identity(),
1482                            });
1483                        }
1484                        3 => {
1485                            let forward = self.count()?;
1486                            let reversed = self.count()?;
1487                            let _continuity = self.word()?;
1488                            let surface = self.count()?;
1489                            let _location = self.count()?;
1490                            let lo = self.number()?;
1491                            let hi = self.number()?;
1492                            let (Some(f), Some(r), Some(on)) = (
1493                                built.pcurves.get(forward - 1).copied(),
1494                                built.pcurves.get(reversed - 1).copied(),
1495                                built.surfaces.get(surface - 1).copied(),
1496                            ) else {
1497                                ogeom_bail!(Construction, "an edge names a seam that is not there");
1498                            };
1499                            data.add(EdgeRepr::Seam {
1500                                forward: f,
1501                                reversed: r,
1502                                range: (lo, hi),
1503                                surface: on,
1504                                location: Location::identity(),
1505                            });
1506                        }
1507                        4 => {
1508                            let _continuity = self.word()?;
1509                            for _ in 0..4 {
1510                                let _ = self.count()?;
1511                            }
1512                        }
1513                        5 => {
1514                            let _ = self.count()?;
1515                            let _ = self.count()?;
1516                        }
1517                        6 => {
1518                            for _ in 0..3 {
1519                                let _ = self.count()?;
1520                            }
1521                        }
1522                        7 => {
1523                            for _ in 0..4 {
1524                                let _ = self.count()?;
1525                            }
1526                        }
1527                        other => ogeom_bail!(
1528                            Construction,
1529                            "an edge representation of kind {other} is not one this reads"
1530                        ),
1531                    }
1532                }
1533                // The file states whether its representations agree on
1534                // parameterization. That is the writer's claim about its own
1535                // data, and it is checked here rather than believed: the
1536                // representations are evaluated against each other, and the
1537                // claim is only re-established when they actually agree.
1538                data.assert_same_parameter(agree_on_parameter(model, &data, tol));
1539                Record::Edge(Box::new(data))
1540            }
1541            "Fa" => {
1542                let _natural = self.flag()?;
1543                let tolerance = self.number()?;
1544                let surface = self.count()?;
1545                let _location = self.count()?;
1546                if self.peek() == Some("2") {
1547                    let _ = self.word()?;
1548                    let _triangulation = self.count()?;
1549                }
1550                let Some(on) = built.surfaces.get(surface.max(1) - 1).copied() else {
1551                    ogeom_bail!(Construction, "a face names surface {surface}");
1552                };
1553                Record::Face(Box::new(FaceData {
1554                    surface: on,
1555                    location: Location::identity(),
1556                    tolerance: Tolerance::new(tolerance.max(tol.confusion()))?,
1557                    natural_restriction: false,
1558                    triangulation: None,
1559                }))
1560            }
1561            "Wi" => Record::Container(ShapeType::Wire),
1562            "Sh" => Record::Container(ShapeType::Shell),
1563            "So" => Record::Container(ShapeType::Solid),
1564            "CS" => Record::Container(ShapeType::CompSolid),
1565            "Co" => Record::Container(ShapeType::Compound),
1566            other => ogeom_bail!(Construction, "`{other}` is not a shape record this reads"),
1567        };
1568
1569        // The flag word, which says nothing about the shape, then the
1570        // children.
1571        let _flags = self.word()?;
1572        let mut children = Vec::new();
1573        while self.peek() != Some("*") {
1574            let (orientation, number, location) = self.reference()?;
1575            let Some(child) = pick(shapes, number, total) else {
1576                ogeom_bail!(
1577                    Construction,
1578                    "a record names subshape {number}, which is not above it"
1579                );
1580            };
1581            children.push(placed(child, built, location, model)?.composed(orientation));
1582        }
1583        let _ = self.word()?;
1584
1585        Ok(match data {
1586            Record::Vertex(data) => model.add_vertex(data),
1587            Record::Edge(data) => model.add_edge(*data, &children)?,
1588            Record::Face(data) => model.add_face(*data, &children)?,
1589            Record::Container(ShapeType::Wire) => model.add_wire(&children)?,
1590            Record::Container(ShapeType::Shell) => model.add_shell(&children)?,
1591            Record::Container(ShapeType::Solid) => model.add_solid(&children)?,
1592            Record::Container(ShapeType::CompSolid) => model.add_compsolid(&children)?,
1593            Record::Container(_) => model.add_compound(&children)?,
1594        })
1595    }
1596}
1597
1598/// What a record turned out to be, before its children are known.
1599enum Record {
1600    Vertex(VertexData),
1601    Edge(Box<EdgeData>),
1602    Face(Box<FaceData>),
1603    Container(ShapeType),
1604}
1605
1606/// Whether an edge's representations land on the same point at the same
1607/// parameter, which is what the same-parameter claim means.
1608///
1609/// Sampled: nine stations across the edge's own range, each read through
1610/// every representation it carries, all held to the edge's stated tolerance.
1611/// A file whose pcurve was fitted independently of its curve fails this, and
1612/// it should: the claim is what every algorithm downstream relies on.
1613fn agree_on_parameter(model: &Model, data: &EdgeData, tol: Tolerances) -> bool {
1614    use ogeom_geom::Surface as _;
1615    let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
1616        return false;
1617    };
1618    let Some(geometry) = model.geometry().curve(*curve) else {
1619        return false;
1620    };
1621    let allowed = data.tolerance.get().max(tol.confusion());
1622    for representation in &data.representations {
1623        let (pcurve, prange, surface) = match representation {
1624            EdgeRepr::PCurve {
1625                curve,
1626                range,
1627                surface,
1628                ..
1629            } => (*curve, *range, *surface),
1630            EdgeRepr::Seam {
1631                forward,
1632                range,
1633                surface,
1634                ..
1635            } => (*forward, *range, *surface),
1636            _ => continue,
1637        };
1638        let (Some(chart), Some(on)) = (
1639            model.geometry().pcurve(pcurve),
1640            model.geometry().surface(surface),
1641        ) else {
1642            return false;
1643        };
1644        for k in 0..=8 {
1645            let f = f64::from(k) / 8.0;
1646            let t = (range.1 - range.0).mul_add(f, range.0);
1647            let pt = (prange.1 - prange.0).mul_add(f, prange.0);
1648            let (Ok(at), Ok(uv)) = (geometry.point_at(t, tol), chart.point_at(pt, tol)) else {
1649                return false;
1650            };
1651            let Ok(lifted) = on.point_at(uv.x, uv.y, tol) else {
1652                return false;
1653            };
1654            if lifted.distance(at) > allowed {
1655                return false;
1656            }
1657        }
1658    }
1659    true
1660}