Skip to main content

ogeom_io/
brep.rs

1//! The `.brep` interchange text format, both directions.
2//!
3//! An interchange format for boundary representation, implemented from its
4//! published specification. It carries exact geometry and full topology,
5//! which is what makes it a bridge for an application moving onto this
6//! kernel that a mesh cannot be.
7//!
8//! The file is a header followed by tables (placements, geometry, topology),
9//! and the last of those is the interesting one. The header's first line,
10//! the content type, is left out by a writer saving a shape on its own (as
11//! a project archive holds its parts), so the version line alone is enough.
12//! Topology records are written leaves first and *numbered backwards*: the
13//! final record is number one, and a record refers to its children by how
14//! far above it they sit. So a parent can only name children already
15//! written, the file needs no forward references, and a reader can build
16//! the model in a single pass with nothing left dangling.
17//!
18//! ## What crosses
19//!
20//! Everything this kernel's own geometry can say: lines, circles, ellipses,
21//! parabolas, hyperbolas, B-splines, and the trimmed and offset forms over
22//! them, in two dimensions and three; planes, cylinders, cones, spheres,
23//! tori, extrusions, revolutions, B-spline surfaces, and trimmed and offset
24//! forms over those. All of the topology, with every occurrence's
25//! orientation and placement, and edges carrying their curve, their pcurves
26//! and their seams.
27//!
28//! No record carries a reversal, so a curve run backwards crosses as a
29//! forward curve through the same points in the same direction: a circle
30//! or an ellipse with its frame turned, a hyperbola or a parabola with its
31//! `y` turned round, a reversed trim as a trim of its basis reversed. Where
32//! that curve reads the original at a shifted parameter (a conic run
33//! backwards over a window not symmetric about zero), every curve of the
34//! edge is written at the same shift, and the chart of a surface swept from
35//! it moves with it, so the edge's curves still agree at each parameter.
36//!
37//! ## What does not
38//!
39//! One thing the reader deliberately does *not* take on faith: whether an
40//! edge's representations agree on parameterization. That is a claim, the
41//! writing kernel's claim about its own data, and everything downstream
42//! relies on it, so it is measured here instead, each representation
43//! evaluated against the others at matched parameters and the claim
44//! re-established only where they actually land together. A file can
45//! therefore come back with the claim *established* where its writer never
46//! made it, which is the right direction to be wrong in.
47//!
48//! Cached triangulations and the polygons that go with them are parsed and
49//! skipped rather than half-honoured: this kernel tessellates on demand and
50//! keeps its own cache, and a mesh read from a file would be a mesh nobody
51//! could say the deflection of. The bookkeeping flags each record carries
52//! are read and dropped for the same reason: they describe the state of the
53//! writer's own session, not the shape.
54
55use ogeom_core::{FastMap, FastSet};
56use std::borrow::Cow;
57use std::fmt::Write as _;
58
59use ogeom_core::{OgeomResult, Tolerance, Tolerances, ogeom_bail};
60use ogeom_geom::{
61    BSpline2d, BSplineCurve, BSplineSurface, Circle2d, CircleCurve, ConeSurface, Curve,
62    Curve2d as _, Curve3d as _, CylinderSurface, Ellipse2d, EllipseCurve, ExtrusionSurface,
63    Hyperbola2d, HyperbolaCurve, Line2d, LineCurve, Offset2d, OffsetCurve, ParabolaCurve,
64    PlanarCurve, PlaneSurface, Reversible as _, RevolutionSurface, SphereSurface, Surface as _,
65    SurfaceGeometry, TorusSurface, Transformable as _, Trimmed2d, TrimmedCurve, TrimmedSurface,
66};
67use ogeom_math::{
68    Axis, Axis2, Circle, Circle2, Cone, ControlGrid, Cylinder, Direction, Direction2, Ellipse,
69    Ellipse2, Frame, Frame2, Handedness, Hyperbola, Hyperbola2, KnotVector, Matrix3, Parabola,
70    Plane, Point, Point2, Sphere, Torus, Transform, Transform2, TransformKind, Vector, Vector2,
71    Weighted,
72};
73use ogeom_topo::{
74    CurveId, EdgeData, EdgeRepr, FaceData, Location, Model, NodeData, Orientation, PCurveId, Shape,
75    ShapeType, SurfaceId, TShape, TShapeId, VertexData,
76};
77
78/// What the format puts at the top of every file, and what a reader looks
79/// for to know it is one. Data, not prose: these bytes are the format.
80const CONTENT_TYPE: &str = "DBRep_DrawableShape";
81const VERSION: &str = "CASCADE Topology V1, (c) Matra-Datavision";
82/// What every version line opens with, whichever version it names.
83const VERSION_FAMILY: &str = "CASCADE Topology V";
84
85// --- writing -----------------------------------------------------------------
86
87/// Write a shape as interchange text.
88///
89/// # Errors
90///
91/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the
92/// shape carries geometry the format has no record for (a helical curve, a
93/// curve living on a surface, the sinusoidal pcurve an oblique section
94/// leaves on a cylinder), each refused by name rather than approximated into
95/// something the file would claim was exact.
96pub fn write(model: &Model, root: &Shape, tol: Tolerances) -> OgeomResult<String> {
97    let mut tables = Tables::default();
98    tables.gather(model, root, tol)?;
99
100    let mut out = String::new();
101    let _ = writeln!(out, "{CONTENT_TYPE}\n");
102    let _ = writeln!(out, "{VERSION}");
103
104    // Placements. Every location is written as its own matrix rather than as
105    // a composition of others: the composition form exists to let a file
106    // share a chain between many shapes, and nothing here is repeated often
107    // enough for that to pay.
108    let _ = writeln!(out, "Locations {}", tables.locations.len());
109    for location in &tables.locations {
110        let transform = location.composed(model.datums())?;
111        let linear = transform.linear();
112        let scale = transform.scale_factor();
113        let translation = transform.translation_vector();
114        // The record's kind on its own line, then the three rows.
115        let _ = writeln!(out, "1");
116        for row in 0..3 {
117            let mut line = String::new();
118            for column in 0..3 {
119                let _ = write!(line, " {}", real(linear.get(row, column)? * scale));
120            }
121            let _ = write!(
122                line,
123                " {}",
124                real(match row {
125                    0 => translation.x,
126                    1 => translation.y,
127                    _ => translation.z,
128                })
129            );
130            let _ = writeln!(out, "{}", line.trim_start());
131        }
132    }
133
134    let _ = writeln!(out, "Curve2ds {}", tables.pcurves.len());
135    for (id, surface, shift) in &tables.pcurves {
136        let chart = chart(model, *id, *surface, tol)?;
137        let (form, own) = spelled2(&chart, tol)?;
138        if own == *shift {
139            write_pcurve(&mut out, &form)?;
140        } else {
141            let Some(moved) = shifted2(&form, own - shift, tol)? else {
142                ogeom_bail!(Construction, "{UNSHIFTED}");
143            };
144            write_pcurve(&mut out, &moved)?;
145        }
146    }
147
148    let _ = writeln!(out, "Curves {}", tables.curves.len());
149    for (id, shift) in &tables.curves {
150        let Some(geometry) = model.geometry().curve(*id) else {
151            ogeom_bail!(Dangling, "a curve is not in this model");
152        };
153        let (form, own) = spelled(geometry, tol)?;
154        if own == *shift {
155            write_curve(&mut out, &form)?;
156        } else {
157            let Some(moved) = shifted(&form, own - shift, tol)? else {
158                ogeom_bail!(Construction, "{UNSHIFTED}");
159            };
160            write_curve(&mut out, &moved)?;
161        }
162    }
163
164    // The two cached-mesh sections, empty. They are part of the file's
165    // shape whether or not anything is in them.
166    let _ = writeln!(out, "Polygon3D 0");
167    let _ = writeln!(out, "PolygonOnTriangulations 0");
168
169    let _ = writeln!(out, "Surfaces {}", tables.surfaces.len());
170    for id in &tables.surfaces {
171        let Some(geometry) = model.geometry().surface(*id) else {
172            ogeom_bail!(Dangling, "a surface is not in this model");
173        };
174        write_surface(&mut out, geometry, tol)?;
175    }
176    let _ = writeln!(out, "Triangulations 0");
177
178    // Topology, leaves first. Record numbering counts backwards from the
179    // last, so a child written earlier has the larger number.
180    let total = tables.order.len();
181    let _ = writeln!(out, "\nTShapes {total}");
182    let number_of = |node: TShapeId| -> OgeomResult<usize> {
183        let Some(position) = tables.index_of.get(&node) else {
184            ogeom_bail!(Construction, "a subshape was never written");
185        };
186        Ok(total - position)
187    };
188
189    for node_id in &tables.order {
190        let shape = Shape::of(*node_id);
191        let Some(node) = model.node(&shape) else {
192            ogeom_bail!(Dangling, "a shape is not in this model");
193        };
194        write_record(&mut out, model, &shape, &tables, tol)?;
195
196        // Flags, then the children. Seven flags the format asks for and this
197        // kernel does not keep: they record the writing session's own
198        // bookkeeping (whether a shape was visited, modified, checked), and
199        // reading them back as geometry would be reading somebody else's
200        // scratch paper. Orientable is the one that is always true here.
201        let _ = writeln!(out, "\n0001000");
202        let mut line = String::new();
203        for child in node.children() {
204            let _ = write!(
205                line,
206                "{}{} {} ",
207                match child.orientation() {
208                    Orientation::Forward => "+",
209                    Orientation::Reversed => "-",
210                    Orientation::Internal => "i",
211                    Orientation::External => "e",
212                },
213                number_of(child.node())?,
214                tables.location_number(child.location()),
215            );
216        }
217        let _ = writeln!(out, "{line}*");
218    }
219
220    // The final record says how the whole model is oriented and placed.
221    let _ = writeln!(
222        out,
223        "\n{}{} {}",
224        match root.orientation() {
225            Orientation::Forward => "+",
226            Orientation::Reversed => "-",
227            Orientation::Internal => "i",
228            Orientation::External => "e",
229        },
230        number_of(root.node())?,
231        tables.location_number(root.location()),
232    );
233    Ok(out)
234}
235
236/// No record carries a reversal flag, so a curve reaching a record with one
237/// would be written running the other way.
238const BACKWARDS: &str = "a curve run backwards reached a record unspelled";
239
240/// Why an edge is refused when its representations cannot be written over
241/// one parameter.
242const UNSHIFTED: &str = "an edge has a curve the format states only at a shifted \
243                         parameter (a conic run backwards over a window not \
244                         symmetric about zero), and another of its curves needs \
245                         another shift, or has no record that shifts with it";
246
247/// The tables a file's sections are: geometry by the order it is written,
248/// topology leaves first, and every placement anything refers to.
249///
250/// A curve is written at a parameter shift (see [`spelled`]), and a chart
251/// curve on its surface at one, so each is keyed by what it is written as.
252#[derive(Default)]
253struct Tables {
254    curves: Vec<(CurveId, f64)>,
255    pcurves: Vec<(PCurveId, SurfaceId, f64)>,
256    surfaces: Vec<SurfaceId>,
257    locations: Vec<Location>,
258    order: Vec<TShapeId>,
259    index_of: FastMap<TShapeId, usize>,
260    curve_at: FastMap<(CurveId, u64), usize>,
261    pcurve_at: FastMap<(PCurveId, SurfaceId, u64), usize>,
262    surface_at: FastMap<SurfaceId, usize>,
263    location_at: FastMap<Location, usize>,
264    /// The shift each edge's parameter is written at: every one of its
265    /// curves is written at it, so they still agree at each parameter.
266    edge_shift: FastMap<TShapeId, f64>,
267    /// Where each of an edge's charts is written as used: the placement of
268    /// the face's surface as seen from the edge, once per use the faces
269    /// make of it, by the chart's index among the edge's representations.
270    chart_uses: FastMap<TShapeId, Vec<(usize, Location)>>,
271}
272
273impl Tables {
274    /// Walk the shape leaves-first, numbering everything it refers to.
275    fn gather(&mut self, model: &Model, root: &Shape, tol: Tolerances) -> OgeomResult<()> {
276        self.visit(model, root, tol)?;
277        // The file's own shape is placed by a record like any other.
278        self.take_location(root.location());
279        Ok(())
280    }
281
282    fn visit(&mut self, model: &Model, shape: &Shape, tol: Tolerances) -> OgeomResult<()> {
283        if self.index_of.contains_key(&shape.node()) {
284            return Ok(());
285        }
286        let Some(node) = model.node(shape) else {
287            ogeom_bail!(Dangling, "shape is not in this model");
288        };
289        // Children first, so a parent's record can name them by how far
290        // above it they were written.
291        for child in node.children() {
292            self.visit(model, child, tol)?;
293            self.take_location(child.location());
294        }
295        match node.data() {
296            NodeData::Edge(data) => {
297                let shift = edge_shift(model, data, tol)?;
298                if shift != 0.0 {
299                    self.edge_shift.insert(shape.node(), shift);
300                }
301                for representation in &data.representations {
302                    match representation {
303                        EdgeRepr::Curve3d {
304                            curve, location, ..
305                        } => {
306                            self.take_curve(*curve, shift);
307                            self.take_location(location);
308                        }
309                        EdgeRepr::PCurve {
310                            curve,
311                            surface,
312                            location,
313                            ..
314                        } => {
315                            self.take_pcurve(*curve, *surface, shift);
316                            self.take_surface(*surface);
317                            self.take_location(&location.inverted());
318                        }
319                        EdgeRepr::Seam {
320                            forward,
321                            reversed,
322                            surface,
323                            location,
324                            ..
325                        } => {
326                            self.take_pcurve(*forward, *surface, shift);
327                            self.take_pcurve(*reversed, *surface, shift);
328                            self.take_surface(*surface);
329                            self.take_location(&location.inverted());
330                        }
331                        _ => {}
332                    }
333                }
334            }
335            NodeData::Face(data) => {
336                self.take_surface(data.surface);
337                self.take_location(&data.location);
338                // The format names an edge's chart on a face by where the
339                // face's surface sits as seen from the edge: the face's own
340                // placement taken back through the edge's inside the face.
341                for wire in model.children_of(&Shape::of(shape.node()))? {
342                    for edge in model.children_of(&wire)? {
343                        let Some(edge_data) = model.node(&edge).and_then(|n| n.data().as_edge())
344                        else {
345                            continue;
346                        };
347                        let Some(index) = edge_data.representations.iter().position(|r| {
348                            Some(r) == edge_data.pcurve_for(data.surface, edge.location())
349                        }) else {
350                            continue;
351                        };
352                        let seen = edge.location().inverted().then(&data.location);
353                        self.take_location(&seen);
354                        let uses = self.chart_uses.entry(edge.node()).or_default();
355                        if !uses.contains(&(index, seen.clone())) {
356                            uses.push((index, seen));
357                        }
358                    }
359                }
360            }
361            NodeData::Vertex(_) | NodeData::Container => {}
362        }
363        self.index_of.insert(shape.node(), self.order.len());
364        self.order.push(shape.node());
365        Ok(())
366    }
367
368    fn take_curve(&mut self, id: CurveId, shift: f64) {
369        let key = (id, shift.to_bits());
370        if !self.curve_at.contains_key(&key) {
371            self.curve_at.insert(key, self.curves.len());
372            self.curves.push((id, shift));
373        }
374    }
375
376    fn take_pcurve(&mut self, id: PCurveId, surface: SurfaceId, shift: f64) {
377        let key = (id, surface, shift.to_bits());
378        if !self.pcurve_at.contains_key(&key) {
379            self.pcurve_at.insert(key, self.pcurves.len());
380            self.pcurves.push((id, surface, shift));
381        }
382    }
383
384    fn take_surface(&mut self, id: SurfaceId) {
385        if !self.surface_at.contains_key(&id) {
386            self.surface_at.insert(id, self.surfaces.len());
387            self.surfaces.push(id);
388        }
389    }
390
391    fn take_location(&mut self, location: &Location) {
392        if location.is_identity() || self.location_at.contains_key(location) {
393            return;
394        }
395        self.location_at
396            .insert(location.clone(), self.locations.len());
397        self.locations.push(location.clone());
398    }
399
400    /// The file's number for a placement: one-based, and zero for identity,
401    /// which the format spells as "no placement at all".
402    fn location_number(&self, location: &Location) -> usize {
403        if location.is_identity() {
404            return 0;
405        }
406        self.location_at.get(location).map_or(0, |at| at + 1)
407    }
408
409    fn curve_number(&self, id: CurveId, shift: f64) -> usize {
410        self.curve_at
411            .get(&(id, shift.to_bits()))
412            .map_or(0, |at| at + 1)
413    }
414
415    fn pcurve_number(&self, id: PCurveId, surface: SurfaceId, shift: f64) -> usize {
416        self.pcurve_at
417            .get(&(id, surface, shift.to_bits()))
418            .map_or(0, |at| at + 1)
419    }
420
421    fn surface_number(&self, id: SurfaceId) -> usize {
422        self.surface_at.get(&id).map_or(0, |at| at + 1)
423    }
424}
425
426/// One topology record's own data: everything before the flag word.
427fn write_record(
428    out: &mut String,
429    model: &Model,
430    shape: &Shape,
431    tables: &Tables,
432    tol: Tolerances,
433) -> OgeomResult<()> {
434    let Some(node) = model.node(shape) else {
435        ogeom_bail!(Dangling, "a shape is not in this model");
436    };
437    match node.data() {
438        NodeData::Vertex(data) => {
439            let _ = writeln!(out, "Ve");
440            let _ = writeln!(out, "{}", real(data.tolerance.get()));
441            let _ = writeln!(
442                out,
443                "{} {} {}",
444                real(data.point.x),
445                real(data.point.y),
446                real(data.point.z)
447            );
448            // A vertex's parameters on the curves through it are recoverable
449            // by projection and are not written, so the list is empty.
450            let _ = writeln!(out, "0 0");
451        }
452        NodeData::Edge(data) => {
453            let _ = writeln!(out, "Ed");
454            let _ = writeln!(
455                out,
456                " {} {} {} {}",
457                real(data.tolerance.get()),
458                usize::from(data.same_parameter()),
459                1,
460                usize::from(data.degenerate)
461            );
462            let shift = tables.edge_shift.get(&shape.node()).copied().unwrap_or(0.0);
463            for (index, representation) in data.representations.iter().enumerate() {
464                // Each use the faces make of a chart is its own line, at
465                // the placement that use names; a chart no face uses is
466                // written as if its face sat where the edge does.
467                let uses: Vec<&Location> = tables
468                    .chart_uses
469                    .get(&shape.node())
470                    .map(|uses| {
471                        uses.iter()
472                            .filter(|(at, _)| *at == index)
473                            .map(|(_, seen)| seen)
474                            .collect()
475                    })
476                    .unwrap_or_default();
477                let inverted = representation.location().map(Location::inverted);
478                let seen: Vec<&Location> = if uses.is_empty() {
479                    inverted.iter().collect()
480                } else {
481                    uses
482                };
483                match representation {
484                    EdgeRepr::Curve3d {
485                        curve,
486                        range,
487                        location,
488                    } => {
489                        let _ = writeln!(
490                            out,
491                            "1 {} {} {} {}",
492                            tables.curve_number(*curve, shift),
493                            tables.location_number(location),
494                            real(range.0 + shift),
495                            real(range.1 + shift)
496                        );
497                    }
498                    EdgeRepr::PCurve {
499                        curve,
500                        range,
501                        surface,
502                        ..
503                    } => {
504                        for seen in seen {
505                            let _ = writeln!(
506                                out,
507                                "2 {} {} {} {} {}",
508                                tables.pcurve_number(*curve, *surface, shift),
509                                tables.surface_number(*surface),
510                                tables.location_number(seen),
511                                real(range.0 + shift),
512                                real(range.1 + shift)
513                            );
514                        }
515                    }
516                    EdgeRepr::Seam {
517                        forward,
518                        reversed,
519                        range,
520                        surface,
521                        ..
522                    } => {
523                        for seen in seen {
524                            let _ = writeln!(
525                                out,
526                                "3 {} {} C0 {} {} {} {}",
527                                tables.pcurve_number(*forward, *surface, shift),
528                                tables.pcurve_number(*reversed, *surface, shift),
529                                tables.surface_number(*surface),
530                                tables.location_number(seen),
531                                real(range.0 + shift),
532                                real(range.1 + shift)
533                            );
534                        }
535                    }
536                    other => ogeom_bail!(
537                        Construction,
538                        "an edge carries a representation this format has no \
539                         record for: {other:?}"
540                    ),
541                }
542            }
543            let _ = writeln!(out, "0");
544        }
545        NodeData::Face(data) => {
546            let _ = writeln!(out, "Fa");
547            let _ = writeln!(
548                out,
549                "0 {} {} {}",
550                real(data.tolerance.get()),
551                tables.surface_number(data.surface),
552                tables.location_number(&data.location)
553            );
554        }
555        NodeData::Container => {
556            let kind = match model.kind_of(shape)? {
557                ShapeType::Wire => "Wi",
558                ShapeType::Shell => "Sh",
559                ShapeType::Solid => "So",
560                ShapeType::CompSolid => "CS",
561                ShapeType::Compound => "Co",
562                other => ogeom_bail!(Construction, "a {other:?} has no record in this format"),
563            };
564            let _ = writeln!(out, "{kind}\n");
565        }
566    }
567    let _ = tol;
568    Ok(())
569}
570
571/// Write a curve [`spelled`] in the format's terms.
572fn write_curve(out: &mut String, curve: &Curve) -> OgeomResult<()> {
573    let backwards = match curve {
574        Curve::Circle(c) => c.is_reversed(),
575        Curve::Ellipse(e) => e.is_reversed(),
576        Curve::Hyperbola(h) => h.is_reversed(),
577        Curve::Parabola(p) => p.is_reversed(),
578        Curve::Trimmed(t) => t.is_reversed(),
579        _ => false,
580    };
581    if backwards {
582        ogeom_bail!(Construction, "{BACKWARDS}");
583    }
584    match curve {
585        Curve::Line(l) => {
586            let axis = l.axis();
587            let _ = writeln!(
588                out,
589                "1 {} {}",
590                point(axis.location),
591                direction(axis.direction.vector())
592            );
593        }
594        Curve::Circle(c) => {
595            let circle = c.circle();
596            let frame = circle.frame();
597            let _ = writeln!(
598                out,
599                "2 {} {} {} {} {}",
600                point(frame.origin()),
601                direction(frame.z().vector()),
602                direction(frame.x().vector()),
603                direction(frame.y().vector()),
604                real(circle.radius())
605            );
606        }
607        Curve::Ellipse(e) => {
608            let ellipse = e.ellipse();
609            let frame = ellipse.frame();
610            let _ = writeln!(
611                out,
612                "3 {} {} {} {} {} {}",
613                point(frame.origin()),
614                direction(frame.z().vector()),
615                direction(frame.x().vector()),
616                direction(frame.y().vector()),
617                real(ellipse.major_radius()),
618                real(ellipse.minor_radius())
619            );
620        }
621        Curve::Parabola(p) => {
622            let parabola = p.parabola();
623            let frame = parabola.frame();
624            let _ = writeln!(
625                out,
626                "4 {} {} {} {} {}",
627                point(frame.origin()),
628                direction(frame.z().vector()),
629                direction(frame.x().vector()),
630                direction(frame.y().vector()),
631                real(parabola.focal())
632            );
633        }
634        Curve::Hyperbola(h) => {
635            let hyperbola = h.hyperbola();
636            let frame = hyperbola.frame();
637            let _ = writeln!(
638                out,
639                "5 {} {} {} {} {} {}",
640                point(frame.origin()),
641                direction(frame.z().vector()),
642                direction(frame.x().vector()),
643                direction(frame.y().vector()),
644                real(hyperbola.major_radius()),
645                real(hyperbola.minor_radius())
646            );
647        }
648        Curve::BSpline(b) => {
649            let rational = b
650                .control_points()
651                .iter()
652                .any(|c| (c.weight - 1.0).abs() > 0.0);
653            let distinct = b.knots().distinct();
654            let mut line = format!(
655                "7 {} 0  {} {} {}",
656                usize::from(rational),
657                b.knots().degree(),
658                b.control_points().len(),
659                distinct.len()
660            );
661            for control in b.control_points() {
662                let at = control.point();
663                let _ = write!(line, " {}", point(at));
664                if rational {
665                    let _ = write!(line, " {}", real(control.weight));
666                }
667            }
668            let _ = writeln!(out, "{line}");
669            let mut knot_line = String::new();
670            for (value, multiplicity) in distinct {
671                let _ = write!(knot_line, " {} {multiplicity}", real(value));
672            }
673            let _ = writeln!(out, "{knot_line}");
674        }
675        Curve::Trimmed(t) => {
676            let (lo, hi) = t.domain();
677            let _ = writeln!(out, "8 {} {}", real(lo), real(hi));
678            write_curve(out, t.basis())?;
679        }
680        Curve::Offset(o) => {
681            let _ = writeln!(
682                out,
683                "9 {} {}",
684                real(o.distance()),
685                direction(o.reference().vector())
686            );
687            write_curve(out, o.basis())?;
688        }
689        Curve::Helix(_) | Curve::OnSurface(_) => ogeom_bail!(
690            Construction,
691            "the format has no record for a helix or a curve carried on a \
692             surface, and writing one as anything else would be writing a \
693             different curve"
694        ),
695    }
696    Ok(())
697}
698
699/// Write a chart curve [`spelled2`] in the format's terms.
700fn write_pcurve(out: &mut String, curve: &PlanarCurve) -> OgeomResult<()> {
701    let backwards = match curve {
702        PlanarCurve::Circle(c) => c.is_reversed(),
703        PlanarCurve::Ellipse(e) => e.is_reversed(),
704        PlanarCurve::Hyperbola(h) => h.is_reversed(),
705        PlanarCurve::Trimmed(t) => t.is_reversed(),
706        _ => false,
707    };
708    if backwards {
709        ogeom_bail!(Construction, "{BACKWARDS}");
710    }
711    match curve {
712        PlanarCurve::Line(l) => {
713            let axis = l.axis();
714            let _ = writeln!(
715                out,
716                "1 {} {}",
717                point2(axis.location),
718                direction2(axis.direction.vector())
719            );
720        }
721        PlanarCurve::Circle(c) => {
722            let circle = c.circle();
723            let frame = circle.frame();
724            let _ = writeln!(
725                out,
726                "2 {} {} {} {}",
727                point2(frame.origin()),
728                direction2(frame.x().vector()),
729                direction2(frame.y().vector()),
730                real(circle.radius())
731            );
732        }
733        PlanarCurve::Ellipse(e) => {
734            let ellipse = e.ellipse();
735            let frame = ellipse.frame();
736            let _ = writeln!(
737                out,
738                "3 {} {} {} {} {}",
739                point2(frame.origin()),
740                direction2(frame.x().vector()),
741                direction2(frame.y().vector()),
742                real(ellipse.major_radius()),
743                real(ellipse.minor_radius())
744            );
745        }
746        PlanarCurve::Hyperbola(h) => {
747            let hyperbola = h.hyperbola();
748            let frame = hyperbola.frame();
749            let _ = writeln!(
750                out,
751                "5 {} {} {} {} {}",
752                point2(frame.origin()),
753                direction2(frame.x().vector()),
754                direction2(frame.y().vector()),
755                real(hyperbola.major_radius()),
756                real(hyperbola.minor_radius())
757            );
758        }
759        PlanarCurve::BSpline(b) => {
760            let rational = b
761                .control_points()
762                .iter()
763                .any(|c| (c.weight - 1.0).abs() > 0.0);
764            let distinct = b.knots().distinct();
765            let mut line = format!(
766                "7 {} 0  {} {} {}",
767                usize::from(rational),
768                b.knots().degree(),
769                b.control_points().len(),
770                distinct.len()
771            );
772            for control in b.control_points() {
773                let at = control.point();
774                let _ = write!(line, " {}", point2(at));
775                if rational {
776                    let _ = write!(line, " {}", real(control.weight));
777                }
778            }
779            let _ = writeln!(out, "{line}");
780            let mut knot_line = String::new();
781            for (value, multiplicity) in distinct {
782                let _ = write!(knot_line, " {} {multiplicity}", real(value));
783            }
784            let _ = writeln!(out, "{knot_line}");
785        }
786        PlanarCurve::Trimmed(t) => {
787            let (lo, hi) = t.domain();
788            let _ = writeln!(out, "8 {} {}", real(lo), real(hi));
789            write_pcurve(out, t.basis())?;
790        }
791        PlanarCurve::Offset(o) => {
792            let _ = writeln!(out, "9 {}", real(o.distance()));
793            write_pcurve(out, o.basis())?;
794        }
795        PlanarCurve::Trig(_) => ogeom_bail!(
796            Construction,
797            "the format has no record for the sinusoidal chart curve an \
798             oblique section leaves on a cylinder; writing it as a spline \
799             would be writing a fit and calling it exact"
800        ),
801    }
802    Ok(())
803}
804
805fn write_surface(out: &mut String, surface: &SurfaceGeometry, tol: Tolerances) -> OgeomResult<()> {
806    // The elementary surfaces all begin the same way: an origin and the
807    // frame's three directions, normal first.
808    let seat = |frame: &Frame| {
809        format!(
810            "{} {} {} {}",
811            point(frame.origin()),
812            direction(frame.z().vector()),
813            direction(frame.x().vector()),
814            direction(frame.y().vector())
815        )
816    };
817    match surface {
818        SurfaceGeometry::Plane(p) => {
819            let _ = writeln!(out, "1 {}", seat(&p.plane().frame()));
820        }
821        SurfaceGeometry::Cylinder(c) => {
822            let _ = writeln!(
823                out,
824                "2 {} {}",
825                seat(&c.cylinder().frame()),
826                real(c.cylinder().radius())
827            );
828        }
829        SurfaceGeometry::Cone(c) => {
830            let cone = c.cone();
831            let _ = writeln!(
832                out,
833                "3 {} {} {}",
834                seat(&cone.frame()),
835                real(cone.reference_radius()),
836                real(cone.half_angle())
837            );
838        }
839        SurfaceGeometry::Sphere(s) => {
840            let _ = writeln!(
841                out,
842                "4 {} {}",
843                seat(&s.sphere().frame()),
844                real(s.sphere().radius())
845            );
846        }
847        SurfaceGeometry::Torus(t) => {
848            let torus = t.torus();
849            let _ = writeln!(
850                out,
851                "5 {} {} {}",
852                seat(&torus.frame()),
853                real(torus.major_radius()),
854                real(torus.minor_radius())
855            );
856        }
857        SurfaceGeometry::Extrusion(e) => {
858            let _ = writeln!(out, "6 {}", direction(e.direction().vector()));
859            write_curve(out, &spelled(e.curve(), tol)?.0)?;
860        }
861        SurfaceGeometry::Revolution(r) => {
862            let axis = r.axis();
863            let _ = writeln!(
864                out,
865                "7 {} {}",
866                point(axis.location),
867                direction(axis.direction.vector())
868            );
869            write_curve(out, &spelled(r.curve(), tol)?.0)?;
870        }
871        SurfaceGeometry::BSpline(b) => {
872            let grid = b.grid();
873            let rational = grid.points().iter().any(|c| (c.weight - 1.0).abs() > 0.0);
874            let u_distinct = b.u_knots().distinct();
875            let v_distinct = b.v_knots().distinct();
876            let _ = writeln!(
877                out,
878                "9 {} {} 0 0 {} {} {} {} {} {}",
879                usize::from(rational),
880                usize::from(rational),
881                b.u_knots().degree(),
882                b.v_knots().degree(),
883                grid.u_count(),
884                grid.v_count(),
885                u_distinct.len(),
886                v_distinct.len()
887            );
888            // Poles run u-major: all of one u row's v values, then the next.
889            for i in 0..grid.u_count() {
890                let mut line = String::new();
891                for j in 0..grid.v_count() {
892                    let control = grid.points()[i * grid.v_count() + j];
893                    let at = control.point();
894                    let _ = write!(line, " {}", point(at));
895                    if rational {
896                        let _ = write!(line, " {}", real(control.weight));
897                    }
898                }
899                let _ = writeln!(out, "{line}");
900            }
901            let mut line = String::new();
902            for (value, multiplicity) in u_distinct {
903                let _ = write!(line, " {} {multiplicity}", real(value));
904            }
905            let _ = writeln!(out, "{line}");
906            let mut line = String::new();
907            for (value, multiplicity) in v_distinct {
908                let _ = write!(line, " {} {multiplicity}", real(value));
909            }
910            let _ = writeln!(out, "{line}");
911        }
912        SurfaceGeometry::Trimmed(t) => {
913            let ((u0, u1), (v0, v1)) = t.domain();
914            let moved = chart_shift(t.basis(), tol)?;
915            let ((u0, u1), (v0, v1)) = ((u0 + moved.x, u1 + moved.x), (v0 + moved.y, v1 + moved.y));
916            let _ = writeln!(
917                out,
918                "10 {} {} {} {}",
919                real(u0),
920                real(u1),
921                real(v0),
922                real(v1)
923            );
924            write_surface(out, t.basis(), tol)?;
925        }
926        SurfaceGeometry::Offset(o) => {
927            let _ = writeln!(out, "11 {}", real(o.distance()));
928            write_surface(out, o.basis(), tol)?;
929        }
930    }
931    Ok(())
932}
933
934/// A number, written so it reads back as itself.
935fn real(v: f64) -> String {
936    let mut text = format!("{v:?}");
937    if text.ends_with(".0") {
938        text.truncate(text.len() - 2);
939    }
940    text
941}
942
943fn point(p: Point) -> String {
944    format!("{} {} {}", real(p.x), real(p.y), real(p.z))
945}
946
947fn point2(p: Point2) -> String {
948    format!("{} {}", real(p.x), real(p.y))
949}
950
951fn direction(v: Vector) -> String {
952    format!("{} {} {}", real(v.x), real(v.y), real(v.z))
953}
954
955fn direction2(v: Vector2) -> String {
956    format!("{} {}", real(v.x), real(v.y))
957}
958
959// --- reversal in the format's terms ------------------------------------------
960
961/// A curve as the format can state it: the same points in the same
962/// direction, with no reversal flag, since no record carries one. The
963/// curve's point at `u` is the spelling's at `u + shift`.
964///
965/// A circle or an ellipse run backwards is its conic with the frame turned
966/// half a turn about `x`, at the same parameter. A hyperbola or a parabola
967/// run backwards over `[lo, hi]` reads its conic at `lo + hi - u`, which is
968/// the conic with its `y` turned round read at `u - lo - hi`: the same
969/// parameter when the domain is symmetric about zero, and otherwise that
970/// conic trimmed to the mirrored domain `[-hi, -lo]`, shifted. A reversed
971/// trim over `[s, e]` reads its basis at `s + e - u`, which is the reversed
972/// basis read `lo + hi - s - e` further on. Where the reversed basis can
973/// take that shift into its own geometry ([`shifted`]), the parameter is
974/// kept.
975fn spelled(curve: &Curve, tol: Tolerances) -> OgeomResult<(Cow<'_, Curve>, f64)> {
976    let owned = |c: Curve| Cow::Owned(c);
977    Ok(match curve {
978        Curve::Circle(c) if c.is_reversed() => (owned(c.forward(tol)?.into()), 0.0),
979        Curve::Ellipse(e) if e.is_reversed() => (owned(e.forward(tol)?.into()), 0.0),
980        Curve::Hyperbola(h) if h.is_reversed() => {
981            let (lo, hi) = h.domain();
982            let conic = h.hyperbola();
983            let turned = Hyperbola::new(
984                turned_about_x(conic.frame(), tol)?,
985                conic.major_radius(),
986                conic.minor_radius(),
987                tol,
988            )?;
989            let (form, shift) = mirrored(
990                HyperbolaCurve::over(turned, -hi, -lo)?.into(),
991                (lo, hi),
992                tol,
993            )?;
994            (owned(form), shift)
995        }
996        Curve::Parabola(p) if p.is_reversed() => {
997            let (lo, hi) = p.domain();
998            let conic = p.parabola();
999            let turned = Parabola::new(turned_about_x(conic.frame(), tol)?, conic.focal(), tol)?;
1000            let (form, shift) =
1001                mirrored(ParabolaCurve::over(turned, -hi, -lo)?.into(), (lo, hi), tol)?;
1002            (owned(form), shift)
1003        }
1004        Curve::Trimmed(t) => {
1005            let (s, e) = t.domain();
1006            let (basis, shift) = if t.is_reversed() {
1007                let (lo, hi) = t.basis().domain();
1008                let reversed = t.basis().reversed();
1009                let (basis, shift) = spelled(&reversed, tol)?;
1010                let basis = basis.into_owned();
1011                (Cow::Owned(basis), shift + ((lo + hi) - (s + e)))
1012            } else {
1013                spelled(t.basis(), tol)?
1014            };
1015            if let Cow::Borrowed(_) = basis {
1016                (Cow::Borrowed(curve), 0.0)
1017            } else if shift == 0.0 {
1018                (
1019                    owned(TrimmedCurve::new(basis.into_owned(), s, e, tol)?.into()),
1020                    0.0,
1021                )
1022            } else if let Some(moved) = shifted(&basis, shift, tol)? {
1023                (owned(TrimmedCurve::new(moved, s, e, tol)?.into()), 0.0)
1024            } else {
1025                let trimmed = TrimmedCurve::new(basis.into_owned(), s + shift, e + shift, tol)?;
1026                (owned(trimmed.into()), shift)
1027            }
1028        }
1029        Curve::Offset(o) => match spelled(o.basis(), tol)? {
1030            (Cow::Borrowed(_), _) => (Cow::Borrowed(curve), 0.0),
1031            (basis, shift) => {
1032                let offset = OffsetCurve::new(basis.into_owned(), o.distance(), o.reference())?;
1033                (owned(Curve::Offset(Box::new(offset))), shift)
1034            }
1035        },
1036        other => (Cow::Borrowed(other), 0.0),
1037    })
1038}
1039
1040/// A conic run backwards over `domain`, given as its turned conic over the
1041/// mirrored domain: the parameter is kept where the domain is symmetric
1042/// about zero, and the turned conic is trimmed to the mirrored domain, the
1043/// parameter shifted, where it is not.
1044fn mirrored(turned: Curve, (lo, hi): (f64, f64), tol: Tolerances) -> OgeomResult<(Curve, f64)> {
1045    let shift = -(lo + hi);
1046    Ok(if shift == 0.0 {
1047        (turned, 0.0)
1048    } else {
1049        (TrimmedCurve::new(turned, -hi, -lo, tol)?.into(), shift)
1050    })
1051}
1052
1053/// A frame turned half a turn about its own `x`: the same `x`, `y` and the
1054/// normal turned round, so a conic in it is the one in `frame` read at the
1055/// negated parameter.
1056fn turned_about_x(frame: Frame, tol: Tolerances) -> OgeomResult<Frame> {
1057    Frame::from_axes(
1058        frame.origin(),
1059        frame.x(),
1060        frame.y().reversed(),
1061        frame.z().reversed(),
1062        tol,
1063    )
1064}
1065
1066/// A spelled curve read `by` further on: the curve whose point at `u` is
1067/// this one's at `u + by`, where a record states it in closed form. A
1068/// line's origin moves along it, a circle's frame turns about its normal,
1069/// a spline's knots move, and a trim or an offset follows its basis. An
1070/// ellipse's eccentric angle, a hyperbola's and a parabola's parameter do
1071/// not shift within their records, and give `None`.
1072fn shifted(curve: &Curve, by: f64, tol: Tolerances) -> OgeomResult<Option<Curve>> {
1073    if by == 0.0 {
1074        return Ok(Some(curve.clone()));
1075    }
1076    Ok(match curve {
1077        Curve::Line(l) => {
1078            let axis = l.axis();
1079            let (lo, hi) = l.domain();
1080            let moved = Axis::new(axis.point_at(by), axis.direction);
1081            Some(LineCurve::over(moved, lo - by, hi - by)?.into())
1082        }
1083        Curve::Circle(c) if !c.is_reversed() => {
1084            let circle = c.circle();
1085            let frame = turned_by(circle.frame(), by, tol)?;
1086            Some(CircleCurve::new(Circle::new(frame, circle.radius(), tol)?).into())
1087        }
1088        Curve::BSpline(b) => {
1089            let knots = b.knots().knots().iter().map(|k| k - by).collect();
1090            let knots = KnotVector::new(knots, b.knots().degree())?;
1091            Some(BSplineCurve::rational(knots, b.control_points().to_vec())?.into())
1092        }
1093        Curve::Trimmed(t) if !t.is_reversed() => match shifted(t.basis(), by, tol)? {
1094            Some(basis) => {
1095                let (s, e) = t.domain();
1096                Some(TrimmedCurve::new(basis, s - by, e - by, tol)?.into())
1097            }
1098            None => None,
1099        },
1100        Curve::Offset(o) => match shifted(o.basis(), by, tol)? {
1101            Some(basis) => Some(Curve::Offset(Box::new(OffsetCurve::new(
1102                basis,
1103                o.distance(),
1104                o.reference(),
1105            )?))),
1106            None => None,
1107        },
1108        _ => None,
1109    })
1110}
1111
1112/// `frame` turned by `angle` about its normal: a circle in it is the one in
1113/// `frame` read `angle` further on.
1114fn turned_by(frame: Frame, angle: f64, tol: Tolerances) -> OgeomResult<Frame> {
1115    let (sin, cos) = angle.sin_cos();
1116    let (x, y) = (frame.x().vector(), frame.y().vector());
1117    Frame::from_axes(
1118        frame.origin(),
1119        Direction::new(x * cos + y * sin, tol)?,
1120        Direction::new(y * cos - x * sin, tol)?,
1121        frame.z(),
1122        tol,
1123    )
1124}
1125
1126/// A chart curve as the format can state it, as [`spelled`] in space. A
1127/// circle or an ellipse run backwards is its conic with the frame's `y`
1128/// turned round, a mirror-image frame the record states as it is. A
1129/// hyperbola run backwards is the branch with its `y` turned round, read
1130/// at `u - lo - hi`.
1131fn spelled2(curve: &PlanarCurve, tol: Tolerances) -> OgeomResult<(Cow<'_, PlanarCurve>, f64)> {
1132    let owned = |c: PlanarCurve| Cow::Owned(c);
1133    Ok(match curve {
1134        PlanarCurve::Circle(c) if c.is_reversed() => {
1135            let circle = c.circle();
1136            let frame = mirrored_frame(circle.frame(), tol)?;
1137            let turned = Circle2d::new(Circle2::new(frame, circle.radius(), tol)?);
1138            (owned(turned.into()), 0.0)
1139        }
1140        PlanarCurve::Ellipse(e) if e.is_reversed() => {
1141            let ellipse = e.ellipse();
1142            let frame = mirrored_frame(ellipse.frame(), tol)?;
1143            let turned = Ellipse2::new(frame, ellipse.major_radius(), ellipse.minor_radius(), tol)?;
1144            (owned(Ellipse2d::new(turned).into()), 0.0)
1145        }
1146        PlanarCurve::Hyperbola(h) if h.is_reversed() => {
1147            let (lo, hi) = h.domain();
1148            let conic = h.hyperbola();
1149            let turned = Hyperbola2::new(
1150                mirrored_frame(conic.frame(), tol)?,
1151                conic.major_radius(),
1152                conic.minor_radius(),
1153                tol,
1154            )?;
1155            let turned: PlanarCurve = Hyperbola2d::over(turned, -hi, -lo)?.into();
1156            let shift = -(lo + hi);
1157            if shift == 0.0 {
1158                (owned(turned), 0.0)
1159            } else {
1160                (owned(Trimmed2d::new(turned, -hi, -lo, tol)?.into()), shift)
1161            }
1162        }
1163        PlanarCurve::Trimmed(t) => {
1164            let (s, e) = t.domain();
1165            let (basis, shift) = if t.is_reversed() {
1166                let (lo, hi) = t.basis().domain();
1167                let reversed = t.basis().reversed();
1168                let (basis, shift) = spelled2(&reversed, tol)?;
1169                let basis = basis.into_owned();
1170                (Cow::Owned(basis), shift + ((lo + hi) - (s + e)))
1171            } else {
1172                spelled2(t.basis(), tol)?
1173            };
1174            if let Cow::Borrowed(_) = basis {
1175                (Cow::Borrowed(curve), 0.0)
1176            } else if shift == 0.0 {
1177                (
1178                    owned(Trimmed2d::new(basis.into_owned(), s, e, tol)?.into()),
1179                    0.0,
1180                )
1181            } else if let Some(moved) = shifted2(&basis, shift, tol)? {
1182                (owned(Trimmed2d::new(moved, s, e, tol)?.into()), 0.0)
1183            } else {
1184                let trimmed = Trimmed2d::new(basis.into_owned(), s + shift, e + shift, tol)?;
1185                (owned(trimmed.into()), shift)
1186            }
1187        }
1188        PlanarCurve::Offset(o) => match spelled2(o.basis(), tol)? {
1189            (Cow::Borrowed(_), _) => (Cow::Borrowed(curve), 0.0),
1190            (basis, shift) => {
1191                let offset = Offset2d::new(basis.into_owned(), o.distance())?;
1192                (owned(PlanarCurve::Offset(Box::new(offset))), shift)
1193            }
1194        },
1195        other => (Cow::Borrowed(other), 0.0),
1196    })
1197}
1198
1199/// `frame` with its `y` turned round.
1200fn mirrored_frame(frame: Frame2, tol: Tolerances) -> OgeomResult<Frame2> {
1201    Frame2::from_axes(frame.origin(), frame.x(), frame.y().reversed(), tol)
1202}
1203
1204/// A spelled chart curve read `by` further on, as [`shifted`] in space.
1205fn shifted2(curve: &PlanarCurve, by: f64, tol: Tolerances) -> OgeomResult<Option<PlanarCurve>> {
1206    if by == 0.0 {
1207        return Ok(Some(curve.clone()));
1208    }
1209    Ok(match curve {
1210        PlanarCurve::Line(l) => {
1211            let axis = l.axis();
1212            let (lo, hi) = l.domain();
1213            let moved = Axis2::new(axis.point_at(by), axis.direction);
1214            Some(Line2d::over(moved, lo - by, hi - by)?.into())
1215        }
1216        PlanarCurve::Circle(c) if !c.is_reversed() => {
1217            let circle = c.circle();
1218            let frame = circle.frame();
1219            let (sin, cos) = by.sin_cos();
1220            let (x, y) = (frame.x().vector(), frame.y().vector());
1221            let frame = Frame2::from_axes(
1222                frame.origin(),
1223                Direction2::new(x * cos + y * sin, tol)?,
1224                Direction2::new(y * cos - x * sin, tol)?,
1225                tol,
1226            )?;
1227            Some(Circle2d::new(Circle2::new(frame, circle.radius(), tol)?).into())
1228        }
1229        PlanarCurve::BSpline(b) => {
1230            let knots = b.knots().knots().iter().map(|k| k - by).collect();
1231            let knots = KnotVector::new(knots, b.knots().degree())?;
1232            Some(BSpline2d::rational(knots, b.control_points().to_vec())?.into())
1233        }
1234        PlanarCurve::Trimmed(t) if !t.is_reversed() => match shifted2(t.basis(), by, tol)? {
1235            Some(basis) => {
1236                let (s, e) = t.domain();
1237                Some(Trimmed2d::new(basis, s - by, e - by, tol)?.into())
1238            }
1239            None => None,
1240        },
1241        PlanarCurve::Offset(o) => match shifted2(o.basis(), by, tol)? {
1242            Some(basis) => Some(PlanarCurve::Offset(Box::new(Offset2d::new(
1243                basis,
1244                o.distance(),
1245            )?))),
1246            None => None,
1247        },
1248        _ => None,
1249    })
1250}
1251
1252/// How a surface's chart moves when its generating curve is spelled with a
1253/// shift: an extrusion's `u` and a revolution's `v` are that curve's
1254/// parameter, and move with it.
1255fn chart_shift(surface: &SurfaceGeometry, tol: Tolerances) -> OgeomResult<Vector2> {
1256    Ok(match surface {
1257        SurfaceGeometry::Extrusion(e) => Vector2::new(spelled(e.curve(), tol)?.1, 0.0),
1258        SurfaceGeometry::Revolution(r) => Vector2::new(0.0, spelled(r.curve(), tol)?.1),
1259        SurfaceGeometry::Trimmed(t) => chart_shift(t.basis(), tol)?,
1260        SurfaceGeometry::Offset(o) => chart_shift(o.basis(), tol)?,
1261        _ => Vector2::new(0.0, 0.0),
1262    })
1263}
1264
1265/// Chart curve `id` as written on `surface`, before its spelling: a cone's
1266/// chart runs along its axis here and along its ruling in the format, and a
1267/// chart moves with its surface's generating curve ([`chart_shift`]).
1268fn chart(
1269    model: &Model,
1270    id: PCurveId,
1271    surface: SurfaceId,
1272    tol: Tolerances,
1273) -> OgeomResult<Cow<'_, PlanarCurve>> {
1274    let (Some(geometry), Some(on)) = (
1275        model.geometry().pcurve(id),
1276        model.geometry().surface(surface),
1277    ) else {
1278        ogeom_bail!(Dangling, "a pcurve or its surface is not in this model");
1279    };
1280    let mut chart = match on {
1281        SurfaceGeometry::Cone(cone) => {
1282            Cow::Owned(geometry.stretched(1.0, 1.0 / cone.cone().half_angle().cos(), tol)?)
1283        }
1284        _ => Cow::Borrowed(geometry),
1285    };
1286    let moved = chart_shift(on, tol)?;
1287    if moved != Vector2::ZERO {
1288        chart = Cow::Owned(chart.transformed(&Transform2::translation(moved), tol)?);
1289    }
1290    Ok(chart)
1291}
1292
1293/// The shift an edge's parameter is written at: zero, unless one of its
1294/// curves is spelled only at a shift, and then that one's. Every other
1295/// curve of the edge must then shift with it ([`shifted`]).
1296fn edge_shift(model: &Model, data: &EdgeData, tol: Tolerances) -> OgeomResult<f64> {
1297    for representation in &data.representations {
1298        let own = |id: PCurveId, surface: SurfaceId| -> OgeomResult<f64> {
1299            let chart = chart(model, id, surface, tol)?;
1300            Ok(spelled2(&chart, tol)?.1)
1301        };
1302        let shifts = match representation {
1303            EdgeRepr::Curve3d { curve, .. } => {
1304                let Some(geometry) = model.geometry().curve(*curve) else {
1305                    ogeom_bail!(Dangling, "a curve is not in this model");
1306                };
1307                [spelled(geometry, tol)?.1, 0.0]
1308            }
1309            EdgeRepr::PCurve { curve, surface, .. } => [own(*curve, *surface)?, 0.0],
1310            EdgeRepr::Seam {
1311                forward,
1312                reversed,
1313                surface,
1314                ..
1315            } => [own(*forward, *surface)?, own(*reversed, *surface)?],
1316            _ => continue,
1317        };
1318        if let Some(shift) = shifts.into_iter().find(|s| *s != 0.0) {
1319            return Ok(shift);
1320        }
1321    }
1322    Ok(0.0)
1323}
1324
1325// --- reading -----------------------------------------------------------------
1326
1327/// Read a shape from interchange text, into a model of its own.
1328///
1329/// # Errors
1330///
1331/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the
1332/// text is not this format, or carries a record this does not read, which
1333/// it says by number rather than by shrugging.
1334pub fn read(text: &str, tol: Tolerances) -> OgeomResult<(Model, Shape)> {
1335    let (model, shape, _) = read_numbered(text, tol)?;
1336    Ok((model, shape))
1337}
1338
1339/// A shape's edges and faces in the order the file numbers them.
1340///
1341/// An application that names subshapes by number (the fifth edge, the
1342/// second face) counts them the way the file is written: depth first from
1343/// the file's own shape, each record's references in the order written,
1344/// every edge or face counted the first time it is met at a placement.
1345/// Faces come out in that order from exploring the shape read too, but
1346/// edges need not: the file keeps a wire's edges as a set, in whatever
1347/// order its writer met them, and the wire read is walked end to end.
1348#[derive(Debug, Clone, Default)]
1349pub struct Numbering {
1350    /// The edges, first numbered first.
1351    pub edges: Vec<Shape>,
1352    /// The faces, first numbered first.
1353    pub faces: Vec<Shape>,
1354}
1355
1356/// [`read`], with the edges and faces as the file numbers them.
1357///
1358/// # Errors
1359///
1360/// As [`read`].
1361pub fn read_numbered(text: &str, tol: Tolerances) -> OgeomResult<(Model, Shape, Numbering)> {
1362    // A file written for the drawing tool opens with the content-type line;
1363    // one written straight from a shape (what project archives hold) opens
1364    // with the version line, often after a blank one.
1365    let mut lines = text.lines().skip_while(|line| line.trim().is_empty());
1366    let Some(first) = lines.next() else {
1367        ogeom_bail!(Construction, "the file is empty");
1368    };
1369    let first = first.trim();
1370    if first != CONTENT_TYPE && !first.starts_with(VERSION_FAMILY) {
1371        ogeom_bail!(
1372            Construction,
1373            "this is not the interchange format: it opens with `{first}`"
1374        );
1375    }
1376    // The version line is whatever the writer put there; every version this
1377    // reads differs only in sections this skips.
1378    let rest: String = lines.collect::<Vec<_>>().join("\n");
1379    let Some(at) = rest.find("Locations") else {
1380        ogeom_bail!(Construction, "the file has no placement table");
1381    };
1382    let mut cursor = Cursor::new(&rest[at..]);
1383    let mut model = Model::new();
1384    let mut built = Built::default();
1385
1386    cursor.expect("Locations")?;
1387    let count = cursor.count()?;
1388    for _ in 0..count {
1389        let kind = cursor.count()?;
1390        match kind {
1391            1 => {
1392                let mut rows = [[0.0_f64; 3]; 3];
1393                let mut translation = [0.0_f64; 3];
1394                for (row, offset) in rows.iter_mut().zip(&mut translation) {
1395                    for cell in row.iter_mut() {
1396                        *cell = cursor.number()?;
1397                    }
1398                    *offset = cursor.number()?;
1399                }
1400                let transform = transform_of(rows, translation, tol)?;
1401                // One datum per placement that moves anything, so two
1402                // shapes placed by it are at the same place structurally;
1403                // one that moves nothing is left out of every chain.
1404                built
1405                    .chains
1406                    .push(if transform.kind() == TransformKind::Identity {
1407                        Location::identity()
1408                    } else {
1409                        Location::of(model.add_datum(transform))
1410                    });
1411                built.locations.push(transform);
1412            }
1413            2 => {
1414                // A composition of placements already read, each raised to a
1415                // power, ending at a zero. Each factor applies after the ones
1416                // listed before it: the first is the innermost. The chain
1417                // keeps the factors, as the file does, so placements built
1418                // from the same factors are the same placement.
1419                let mut composed = Transform::IDENTITY;
1420                let mut chain = Location::identity();
1421                loop {
1422                    let index = cursor.count()?;
1423                    if index == 0 {
1424                        break;
1425                    }
1426                    let power: i64 = cursor.integer()?;
1427                    let (Some(base), Some(factor)) = (
1428                        built.locations.get(index - 1).copied(),
1429                        built.chains.get(index - 1).cloned(),
1430                    ) else {
1431                        ogeom_bail!(
1432                            Construction,
1433                            "a composed placement names placement {index}, which is not above it"
1434                        );
1435                    };
1436                    let (step, factor) = if power < 0 {
1437                        (base.inverse()?, factor.inverted())
1438                    } else {
1439                        (base, factor)
1440                    };
1441                    for _ in 0..power.abs() {
1442                        composed = step * composed;
1443                        chain = factor.then(&chain);
1444                    }
1445                }
1446                built.locations.push(composed);
1447                built.chains.push(chain);
1448            }
1449            other => ogeom_bail!(
1450                Construction,
1451                "placement record {other} is not one this reads"
1452            ),
1453        }
1454    }
1455
1456    cursor.expect("Curve2ds")?;
1457    let count = cursor.count()?;
1458    for _ in 0..count {
1459        let curve = cursor.pcurve(tol)?;
1460        built.pcurves.push(model.geometry_mut().add_pcurve(curve));
1461    }
1462
1463    cursor.expect("Curves")?;
1464    let count = cursor.count()?;
1465    for _ in 0..count {
1466        let curve = cursor.curve(tol)?;
1467        built.curves.push(model.geometry_mut().add_curve(curve));
1468    }
1469
1470    // The cached-mesh sections, counted so they can be stepped over.
1471    cursor.expect("Polygon3D")?;
1472    let count = cursor.count()?;
1473    for _ in 0..count {
1474        cursor.skip_polygon3d()?;
1475    }
1476    cursor.expect("PolygonOnTriangulations")?;
1477    let count = cursor.count()?;
1478    for _ in 0..count {
1479        cursor.skip_polygon_on_triangulation()?;
1480    }
1481
1482    cursor.expect("Surfaces")?;
1483    let count = cursor.count()?;
1484    for _ in 0..count {
1485        let surface = cursor.surface(tol)?;
1486        built
1487            .surfaces
1488            .push(model.geometry_mut().add_surface(surface));
1489    }
1490
1491    cursor.expect("Triangulations")?;
1492    let count = cursor.count()?;
1493    for _ in 0..count {
1494        cursor.skip_triangulation()?;
1495    }
1496
1497    cursor.expect("TShapes")?;
1498    let total = cursor.count()?;
1499    // Records are read in file order and numbered backwards, so the record
1500    // just read is number `total - written`, and every child it names has
1501    // already been built.
1502    let mut shapes: Vec<Shape> = Vec::with_capacity(cursor.room(total));
1503    for _ in 0..total {
1504        let shape = cursor.record(&mut model, &mut built, &shapes, total, tol)?;
1505        shapes.push(shape);
1506    }
1507    // The faces have given every edge its charts: whether those agree with
1508    // the edge's curve can be measured here, and not before.
1509    for edge in &shapes {
1510        let Some(data) = model.node(edge).and_then(|n| n.data().as_edge()) else {
1511            continue;
1512        };
1513        let agrees = agree_on_parameter(&model, data, tol);
1514        if let Some(NodeData::Edge(data)) = model.node_mut(edge).map(TShape::data_mut) {
1515            data.assert_same_parameter(agrees);
1516        }
1517    }
1518    // The last reference is the file's own shape: how the whole model is
1519    // oriented and placed.
1520    let (orientation, number, location) = cursor.reference()?;
1521    let Some(root) = pick(&shapes, number, total) else {
1522        ogeom_bail!(
1523            Construction,
1524            "the file's own shape is numbered {number}, which is not there"
1525        );
1526    };
1527    let sense = built.sense(&root, orientation);
1528    let root = placed(root, &built, location)?.composed(sense);
1529    let numbering = numbered(&model, &built, &shapes, (number, location))?;
1530    Ok((model, root, numbering))
1531}
1532
1533/// How far an unbounded conic runs when it is read.
1534///
1535/// The format's parabola and hyperbola records carry a focus and a frame and
1536/// no parameter window; the window is the *edge's* business, and every edge
1537/// states its own range. So the curve is built over a window wide enough for
1538/// any edge a file of ordinary size could hold, and the edge trims it.
1539const OPEN_EXTENT: f64 = 1e6;
1540
1541/// Everything read so far that later records refer to by number.
1542#[derive(Default)]
1543struct Built {
1544    locations: Vec<Transform>,
1545    /// Each placement as a chain of the file's elementary placements.
1546    chains: Vec<Location>,
1547    curves: Vec<CurveId>,
1548    pcurves: Vec<PCurveId>,
1549    surfaces: Vec<SurfaceId>,
1550    /// Curves and surfaces carried to where a record places them, by the
1551    /// file's curve or surface number and placement number.
1552    placed_curves: FastMap<(usize, usize), CurveId>,
1553    placed_surfaces: FastMap<(usize, usize), SurfaceId>,
1554    /// Each edge's charts as the file states them, held until the faces
1555    /// that use them are read: a chart is bound to a face occurrence, which
1556    /// the edge record alone cannot name.
1557    charts: FastMap<TShapeId, Vec<StatedChart>>,
1558    /// How the file's charts read on a surface whose chart is not the
1559    /// file's own, and the charts carried onto them.
1560    chart_maps: FastMap<SurfaceId, ChartMap>,
1561    mapped_charts: FastMap<(PCurveId, SurfaceId), PCurveId>,
1562    /// Faces on such a surface: each built with its wires walked the other
1563    /// way and used turned over, so it bounds what the file says it does.
1564    turned_faces: FastSet<TShapeId>,
1565    /// Each record's references as the file writes them, in its order: a
1566    /// wire here is a walk, and the file's own order is what numbers its
1567    /// edges.
1568    written: Vec<Vec<(usize, usize)>>,
1569    /// Faces rebuilt where a shell places them, by the face and that
1570    /// placement.
1571    settled: FastMap<(TShapeId, Location), TShapeId>,
1572}
1573
1574/// How a chart curve in the file reads on a surface as it is built here.
1575#[derive(Clone, Copy)]
1576struct ChartMap {
1577    /// The surface was rebuilt right-handed and its chart turned over.
1578    turned: bool,
1579    /// What `v` is multiplied by: a cone's chart runs along the ruling in
1580    /// the file and along the axis here.
1581    v_scale: f64,
1582}
1583
1584/// One chart representation of an edge, in the file's own numbers.
1585struct StatedChart {
1586    /// One curve, or a seam's two (forward use first).
1587    curves: (PCurveId, Option<PCurveId>),
1588    surface: usize,
1589    location: usize,
1590    range: (f64, f64),
1591}
1592
1593impl Built {
1594    /// How a reference uses a shape: as the file says, turned over for a
1595    /// face built turned over.
1596    fn sense(&self, shape: &Shape, stated: Orientation) -> Orientation {
1597        if self.turned_faces.contains(&shape.node()) {
1598            stated.compose(Orientation::Reversed)
1599        } else {
1600            stated
1601        }
1602    }
1603
1604    /// The transform the file's placement number names; zero is none.
1605    fn transform(&self, number: usize) -> OgeomResult<Transform> {
1606        if number == 0 {
1607            return Ok(Transform::IDENTITY);
1608        }
1609        let Some(transform) = self.locations.get(number - 1) else {
1610            ogeom_bail!(
1611                Construction,
1612                "a record names placement {number}, which is not there"
1613            );
1614        };
1615        Ok(*transform)
1616    }
1617
1618    /// The placement the file's number names, as a location.
1619    fn location(&self, number: usize) -> OgeomResult<Location> {
1620        if number == 0 {
1621            return Ok(Location::identity());
1622        }
1623        let Some(chain) = self.chains.get(number - 1) else {
1624            ogeom_bail!(
1625                Construction,
1626                "a record names placement {number}, which is not there"
1627            );
1628        };
1629        Ok(chain.clone())
1630    }
1631
1632    /// Curve `number` where placement `location` puts it.
1633    fn curve(
1634        &mut self,
1635        model: &mut Model,
1636        number: usize,
1637        location: usize,
1638        tol: Tolerances,
1639    ) -> OgeomResult<CurveId> {
1640        let Some(id) = number
1641            .checked_sub(1)
1642            .and_then(|at| self.curves.get(at))
1643            .copied()
1644        else {
1645            ogeom_bail!(Construction, "an edge names curve {number}");
1646        };
1647        let transform = self.transform(location)?;
1648        if transform.kind() == TransformKind::Identity {
1649            return Ok(id);
1650        }
1651        if let Some(placed) = self.placed_curves.get(&(number, location)) {
1652            return Ok(*placed);
1653        }
1654        let Some(curve) = model.geometry().curve(id) else {
1655            ogeom_bail!(Dangling, "a curve read from the file is not in the model");
1656        };
1657        let moved = curve.transformed(&transform, tol)?;
1658        let placed = model.geometry_mut().add_curve(moved);
1659        self.placed_curves.insert((number, location), placed);
1660        Ok(placed)
1661    }
1662
1663    /// Surface `number` where placement `location` puts it, on a
1664    /// right-handed seat.
1665    fn surface(
1666        &mut self,
1667        model: &mut Model,
1668        number: usize,
1669        location: usize,
1670        tol: Tolerances,
1671    ) -> OgeomResult<SurfaceId> {
1672        let Some(id) = number
1673            .checked_sub(1)
1674            .and_then(|at| self.surfaces.get(at))
1675            .copied()
1676        else {
1677            ogeom_bail!(Construction, "a face names surface {number}");
1678        };
1679        if let Some(placed) = self.placed_surfaces.get(&(number, location)) {
1680            return Ok(*placed);
1681        }
1682        let Some(surface) = model.geometry().surface(id) else {
1683            ogeom_bail!(Dangling, "a surface read from the file is not in the model");
1684        };
1685        let transform = self.transform(location)?;
1686        let moved = transform.kind() != TransformKind::Identity;
1687        let surface = if moved {
1688            surface.transformed(&transform, tol)?
1689        } else {
1690            surface.clone()
1691        };
1692        let (surface, rebuilt, turned) = match right_handed(&surface, tol)? {
1693            Some((upright, turned)) => (upright, true, turned),
1694            None => (surface, false, false),
1695        };
1696        let v_scale = match &surface {
1697            SurfaceGeometry::Cone(c) => c.cone().half_angle().cos(),
1698            _ => 1.0,
1699        };
1700        let placed = if moved || rebuilt {
1701            model.geometry_mut().add_surface(surface)
1702        } else {
1703            id
1704        };
1705        if turned || v_scale != 1.0 {
1706            self.chart_maps.insert(placed, ChartMap { turned, v_scale });
1707        }
1708        self.placed_surfaces.insert((number, location), placed);
1709        Ok(placed)
1710    }
1711
1712    /// Whether faces on surface `on` are built turned over.
1713    fn turned(&self, on: SurfaceId) -> bool {
1714        self.chart_maps.get(&on).is_some_and(|map| map.turned)
1715    }
1716
1717    /// A chart curve as it reads on surface `on`, carried by the surface's
1718    /// chart map where it has one.
1719    fn chart_on(
1720        &mut self,
1721        model: &mut Model,
1722        curve: PCurveId,
1723        on: SurfaceId,
1724        tol: Tolerances,
1725    ) -> OgeomResult<PCurveId> {
1726        let Some(map) = self.chart_maps.get(&on).copied() else {
1727            return Ok(curve);
1728        };
1729        if let Some(mapped) = self.mapped_charts.get(&(curve, on)) {
1730            return Ok(*mapped);
1731        }
1732        let Some(geometry) = model.geometry().pcurve(curve) else {
1733            ogeom_bail!(
1734                Dangling,
1735                "a chart curve read from the file is not in the model"
1736            );
1737        };
1738        let mut mapped = geometry.stretched(1.0, map.v_scale, tol)?;
1739        if map.turned {
1740            mapped = mapped.transformed(&turned_chart(), tol)?;
1741        }
1742        let mapped = model.geometry_mut().add_pcurve(mapped);
1743        self.mapped_charts.insert((curve, on), mapped);
1744        Ok(mapped)
1745    }
1746
1747    /// Give each edge of a face the chart the file binds to its use there.
1748    ///
1749    /// The file keys a chart by where the face's surface sits as seen from
1750    /// the edge: the face's own placement, taken back through the edge's
1751    /// placement inside the face. Here a chart is keyed the other way round,
1752    /// by the edge's placement inside the face, on the face's surface
1753    /// already carried to where the face puts it.
1754    fn bind_charts(
1755        &mut self,
1756        model: &mut Model,
1757        (surface, location): (usize, usize),
1758        on: SurfaceId,
1759        wires: &[Shape],
1760        tol: Tolerances,
1761    ) -> OgeomResult<()> {
1762        let face_placement = self.transform(location)?;
1763        for wire in wires {
1764            for edge in model.children_of(wire)? {
1765                let within = edge.location().clone();
1766                let seen = within.composed(model.datums())?.inverse()? * face_placement;
1767                let mut found = None;
1768                for chart in self.charts.get(&edge.node()).map_or(&[][..], Vec::as_slice) {
1769                    if chart.surface == surface
1770                        && self.transform(chart.location)?.is_equal(&seen, tol)
1771                    {
1772                        found = Some((chart.curves, chart.range));
1773                        break;
1774                    }
1775                }
1776                let Some((curves, range)) = found else {
1777                    plane_chart(model, &edge, on, tol)?;
1778                    continue;
1779                };
1780                let representation = match curves {
1781                    (curve, None) => EdgeRepr::PCurve {
1782                        curve: self.chart_on(model, curve, on, tol)?,
1783                        range,
1784                        surface: on,
1785                        location: within.clone(),
1786                    },
1787                    // A seam's sides are named by the edge's sense in the
1788                    // face, which a turned face walks the other way.
1789                    (first, Some(second)) => {
1790                        let (first, second) = (
1791                            self.chart_on(model, first, on, tol)?,
1792                            self.chart_on(model, second, on, tol)?,
1793                        );
1794                        let (forward, reversed) = if self.turned(on) {
1795                            (second, first)
1796                        } else {
1797                            (first, second)
1798                        };
1799                        EdgeRepr::Seam {
1800                            forward,
1801                            reversed,
1802                            range,
1803                            surface: on,
1804                            location: within.clone(),
1805                        }
1806                    }
1807                };
1808                let Some(NodeData::Edge(data)) = model.node_mut(&edge).map(TShape::data_mut) else {
1809                    ogeom_bail!(Construction, "a wire holds something that is not an edge");
1810                };
1811                let held = data
1812                    .representations
1813                    .iter()
1814                    .any(|r| r.surface() == Some(on) && r.location() == Some(&within));
1815                if !held {
1816                    data.add(representation);
1817                }
1818            }
1819        }
1820        Ok(())
1821    }
1822}
1823
1824/// The shape a backward record number names.
1825fn pick(shapes: &[Shape], number: usize, total: usize) -> Option<Shape> {
1826    // Record number `n` counts back from the last: it is the one at position
1827    // `total - n` in file order.
1828    total
1829        .checked_sub(number)
1830        .and_then(|at| shapes.get(at))
1831        .cloned()
1832}
1833
1834/// A transform from the format's three rows and translation column.
1835fn transform_of(
1836    rows: [[f64; 3]; 3],
1837    translation: [f64; 3],
1838    tol: Tolerances,
1839) -> OgeomResult<Transform> {
1840    // The three-by-three carries the scale inside it: its determinant is the
1841    // cube of the uniform factor, and dividing it out leaves the rotation.
1842    let m = Matrix3::new(rows);
1843    let determinant = m.determinant();
1844    if determinant.abs() <= f64::MIN_POSITIVE {
1845        ogeom_bail!(
1846            Construction,
1847            "a placement with a singular matrix places nothing"
1848        );
1849    }
1850    let scale = determinant.cbrt();
1851    let mut unit = [[0.0_f64; 3]; 3];
1852    for row in 0..3 {
1853        for column in 0..3 {
1854            unit[row][column] = rows[row][column] / scale;
1855        }
1856    }
1857    Transform::from_parts(
1858        Matrix3::new(unit),
1859        scale,
1860        Vector::new(translation[0], translation[1], translation[2]),
1861        tol.angular().max(1e-9),
1862    )
1863}
1864
1865/// A surface on a left-handed seat, rebuilt on a right-handed one; `None`
1866/// for one already right-handed, or of a kind with no seat.
1867///
1868/// A plane keeps its chart, its normal taken as `x` crossed with `y`, which
1869/// is the side the file's faces read as theirs; so does a trimmed one. A surface of revolution
1870/// keeps its axis, its turn running the other way: the chart is turned
1871/// over (`u` read as a full turn less `u`), and with it the side its faces
1872/// call outward, which the second value says.
1873fn right_handed(
1874    surface: &SurfaceGeometry,
1875    tol: Tolerances,
1876) -> OgeomResult<Option<(SurfaceGeometry, bool)>> {
1877    use ogeom_geom::Surface as _;
1878    let upright = |frame: Frame| Frame::new(frame.origin(), frame.z(), frame.x(), tol);
1879    let left = |frame: Frame| frame.handedness() == Handedness::Left;
1880    let (_, v) = surface.domain();
1881    Ok(Some(match surface {
1882        SurfaceGeometry::Plane(s) if left(s.plane().frame()) => {
1883            let frame = s.plane().frame();
1884            let (u, v) = s.domain();
1885            let normal = Direction::new(-frame.z().vector(), tol)?;
1886            let frame = Frame::new(frame.origin(), normal, frame.x(), tol)?;
1887            (PlaneSurface::over(Plane::new(frame), u, v)?.into(), false)
1888        }
1889        SurfaceGeometry::Cylinder(s) if left(s.cylinder().frame()) => {
1890            let c = s.cylinder();
1891            let c = Cylinder::new(upright(c.frame())?, c.radius(), tol)?;
1892            (CylinderSurface::new(c, v)?.into(), true)
1893        }
1894        SurfaceGeometry::Cone(s) if left(s.cone().frame()) => {
1895            let c = s.cone();
1896            let c = Cone::new(
1897                upright(c.frame())?,
1898                c.reference_radius(),
1899                c.half_angle(),
1900                tol,
1901            )?;
1902            (ConeSurface::new(c, v)?.into(), true)
1903        }
1904        SurfaceGeometry::Sphere(s) if left(s.sphere().frame()) => {
1905            let b = s.sphere();
1906            (
1907                SphereSurface::new(Sphere::new(upright(b.frame())?, b.radius(), tol)?).into(),
1908                true,
1909            )
1910        }
1911        SurfaceGeometry::Torus(s) if left(s.torus().frame()) => {
1912            let t = s.torus();
1913            let t = Torus::new(upright(t.frame())?, t.major_radius(), t.minor_radius(), tol)?;
1914            (TorusSurface::new(t).into(), true)
1915        }
1916        // A trimmed plane keeps its window, which the rebuilt plane's chart
1917        // shares.
1918        SurfaceGeometry::Trimmed(t) => match right_handed(t.basis(), tol)? {
1919            Some((basis, false)) => {
1920                let (u, v) = surface.domain();
1921                (
1922                    SurfaceGeometry::Trimmed(Box::new(TrimmedSurface::new(basis, u, v, tol)?)),
1923                    false,
1924                )
1925            }
1926            _ => return Ok(None),
1927        },
1928        _ => return Ok(None),
1929    }))
1930}
1931
1932/// The chart of a surface of revolution turned over: `u` read as a full
1933/// turn less `u`, `v` as it was.
1934fn turned_chart() -> Transform2 {
1935    Transform2::line_mirror(Point2::new(core::f64::consts::PI, 0.0), Direction2::X)
1936}
1937
1938/// Give an edge its chart on a plane the file states none for.
1939///
1940/// The format leaves an edge on a plane without one where its writer can
1941/// project the curve whenever it is asked, so a reader has to do the same:
1942/// the image of a curve in a plane's chart is exact for every curve the
1943/// format carries. Anything but a plane, trimmed or not, the file must
1944/// chart itself.
1945fn plane_chart(model: &mut Model, edge: &Shape, on: SurfaceId, tol: Tolerances) -> OgeomResult<()> {
1946    let Some(surface) = model.geometry().surface(on).cloned() else {
1947        return Ok(());
1948    };
1949    let flat = match &surface {
1950        SurfaceGeometry::Plane(_) => true,
1951        SurfaceGeometry::Trimmed(t) => matches!(t.basis(), SurfaceGeometry::Plane(_)),
1952        _ => false,
1953    };
1954    if !flat {
1955        return Ok(());
1956    }
1957    let Some(data) = model.node(edge).and_then(|n| n.data().as_edge()) else {
1958        return Ok(());
1959    };
1960    let within = edge.location().clone();
1961    if data.degenerate
1962        || data
1963            .pcurve_for(on, &within)
1964            .is_some_and(|r| r.location() == Some(&within))
1965    {
1966        return Ok(());
1967    }
1968    let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d().cloned() else {
1969        return Ok(());
1970    };
1971    let Some(geometry) = model.geometry().curve(curve) else {
1972        ogeom_bail!(Dangling, "an edge's curve is not in the model");
1973    };
1974    // The edge's curve where the face uses it.
1975    let geometry = geometry.transformed(&within.composed(model.datums())?, tol)?;
1976    let image = match ogeom_intersect::exact_pcurve_over(&geometry, range, &surface, tol) {
1977        Some(exact) => crate::pcurves::over_range(&exact, range),
1978        None => crate::pcurves::fit_projected_pcurve(&geometry, range, &surface, tol)?.0,
1979    };
1980    let image = model.geometry_mut().add_pcurve(image);
1981    let Some(NodeData::Edge(data)) = model.node_mut(edge).map(TShape::data_mut) else {
1982        return Ok(());
1983    };
1984    data.add(EdgeRepr::PCurve {
1985        curve: image,
1986        range,
1987        surface: on,
1988        location: within,
1989    });
1990    Ok(())
1991}
1992
1993/// The file's numbering of the edges and faces below its own shape.
1994fn numbered(
1995    model: &Model,
1996    built: &Built,
1997    shapes: &[Shape],
1998    root: (usize, usize),
1999) -> OgeomResult<Numbering> {
2000    let total = shapes.len();
2001    let mut numbering = Numbering::default();
2002    let mut met: FastSet<(TShapeId, Location)> = FastSet::default();
2003    // Each entry: a record, where its parent stands, and its own placement
2004    // there.
2005    let mut stack = vec![(root.0, Location::identity(), built.location(root.1)?)];
2006    while let Some((number, outer, own)) = stack.pop() {
2007        let (Some(index), Some(shape)) = (total.checked_sub(number), pick(shapes, number, total))
2008        else {
2009            ogeom_bail!(
2010                Construction,
2011                "a record names subshape {number}, which is not above it"
2012            );
2013        };
2014        let at = outer.then(&own);
2015        let kind = model.kind_of(&shape)?;
2016        if matches!(kind, ShapeType::Edge | ShapeType::Face) {
2017            if !met.insert((shape.node(), at.clone())) {
2018                continue;
2019            }
2020            if kind == ShapeType::Edge {
2021                numbering.edges.push(shape.moved(&at));
2022                continue;
2023            }
2024            // A face rebuilt where its shell placed it is the face there.
2025            numbering
2026                .faces
2027                .push(match built.settled.get(&(shape.node(), own)) {
2028                    Some(rebuilt) => Shape::of(*rebuilt).moved(&outer),
2029                    None => shape.moved(&at),
2030                });
2031        }
2032        if kind == ShapeType::Vertex {
2033            continue;
2034        }
2035        let Some(written) = built.written.get(index) else {
2036            continue;
2037        };
2038        for &(child, location) in written.iter().rev() {
2039            stack.push((child, at.clone(), built.location(location)?));
2040        }
2041    }
2042    Ok(numbering)
2043}
2044
2045/// A face used at a placement, rebuilt where that puts it when the
2046/// placement undoes part of its edges' placements inside it.
2047///
2048/// An edge's chart on a face is found from the edge's placement in the
2049/// whole shape, whose tail is its placement inside the face. Where the
2050/// face's placement cancels against that (a face turned one way, its wire
2051/// turned back) the tail is gone from the chain; the face is then built
2052/// anew in place, its surface carried there and its edges' charts bound to
2053/// the placements they have there.
2054fn settled(
2055    model: &mut Model,
2056    built: &mut Built,
2057    face: Shape,
2058    tol: Tolerances,
2059) -> OgeomResult<Shape> {
2060    if face.location().is_identity() || model.kind_of(&face)? != ShapeType::Face {
2061        return Ok(face);
2062    }
2063    let Some(data) = model.node(&face).and_then(|n| n.data().as_face()).cloned() else {
2064        return Ok(face);
2065    };
2066    let bare = Shape::of(face.node());
2067    let wires = model.children_of(&bare)?;
2068    let mut inside = Vec::new();
2069    for wire in &wires {
2070        inside.push(model.children_of(&wire.oriented(Orientation::Forward))?);
2071    }
2072    let outer = face.location().clone();
2073    if inside
2074        .iter()
2075        .flatten()
2076        .all(|edge| outer.then(edge.location()).ends_with(edge.location()))
2077    {
2078        return Ok(face);
2079    }
2080    let Some(surface) = model.geometry().surface(data.surface) else {
2081        ogeom_bail!(Dangling, "a face's surface is not in the model");
2082    };
2083    let surface = surface.transformed(&outer.composed(model.datums())?, tol)?;
2084    let surface = model.geometry_mut().add_surface(surface);
2085    let mut placed_wires = Vec::with_capacity(wires.len());
2086    for (wire, edges) in wires.iter().zip(&inside) {
2087        let mut placed_edges = Vec::with_capacity(edges.len());
2088        for edge in edges {
2089            let at = outer.then(edge.location());
2090            let chart = model
2091                .node(edge)
2092                .and_then(|n| n.data().as_edge())
2093                .and_then(|d| d.pcurve_for(data.surface, edge.location()))
2094                .cloned();
2095            let bound = match chart {
2096                Some(EdgeRepr::PCurve { curve, range, .. }) => Some(EdgeRepr::PCurve {
2097                    curve,
2098                    range,
2099                    surface,
2100                    location: at.clone(),
2101                }),
2102                Some(EdgeRepr::Seam {
2103                    forward,
2104                    reversed,
2105                    range,
2106                    ..
2107                }) => Some(EdgeRepr::Seam {
2108                    forward,
2109                    reversed,
2110                    range,
2111                    surface,
2112                    location: at.clone(),
2113                }),
2114                _ => None,
2115            };
2116            if let Some(bound) = bound
2117                && let Some(NodeData::Edge(edge_data)) = model.node_mut(edge).map(TShape::data_mut)
2118            {
2119                edge_data.add(bound);
2120            }
2121            placed_edges.push(
2122                Shape::of(edge.node())
2123                    .moved(&at)
2124                    .oriented(edge.orientation()),
2125            );
2126        }
2127        placed_wires.push(model.add_wire(&placed_edges)?.oriented(wire.orientation()));
2128    }
2129    let rebuilt = model.add_face(
2130        FaceData {
2131            surface,
2132            location: Location::identity(),
2133            ..data
2134        },
2135        &placed_wires,
2136    )?;
2137    built.settled.insert((face.node(), outer), rebuilt.node());
2138    Ok(rebuilt.oriented(face.orientation()))
2139}
2140
2141/// A wire's edges in the order they run, each starting where the one
2142/// before it ends; `None` where they do not make one walk.
2143///
2144/// The format keeps a wire's edges as a set, in whatever order its writer
2145/// met them (a prism's side lists its two rims before the ruling between
2146/// them), while a wire here is a walk.
2147fn walked(model: &Model, edges: &[Shape]) -> OgeomResult<Option<Vec<Shape>>> {
2148    // Where each use of an edge starts and ends: its vertices as the use
2149    // meets them.
2150    let mut ends = Vec::with_capacity(edges.len());
2151    for edge in edges {
2152        let mut start = None;
2153        let mut end = None;
2154        for vertex in model.children_of(edge)? {
2155            let at = Some((vertex.node(), vertex.location().clone()));
2156            match vertex.orientation() {
2157                Orientation::Forward => start = at,
2158                Orientation::Reversed => end = at,
2159                Orientation::Internal | Orientation::External => {}
2160            }
2161        }
2162        let (Some(start), Some(end)) = (start, end) else {
2163            return Ok(None);
2164        };
2165        ends.push((start, end));
2166    }
2167    // A walk through every edge once, closed loops included wherever they
2168    // sit: each step leaves by an edge not yet taken from the vertex it is
2169    // at, and a step that finds none settles its edge into the walk
2170    // (Hierholzer's way), so a loop met mid-walk is spliced in rather
2171    // than left over.
2172    let mut leaving: FastMap<&(TShapeId, Location), Vec<usize>> = FastMap::default();
2173    for (i, (start, _)) in ends.iter().enumerate().rev() {
2174        leaving.entry(start).or_default().push(i);
2175    }
2176    let Some((first, _)) = ends.first() else {
2177        return Ok(None);
2178    };
2179    let mut stack: Vec<(&(TShapeId, Location), Option<usize>)> = vec![(first, None)];
2180    let mut order = Vec::with_capacity(edges.len());
2181    while let Some(&(at, by)) = stack.last() {
2182        if let Some(next) = leaving.get_mut(at).and_then(Vec::pop) {
2183            stack.push((&ends[next].1, Some(next)));
2184        } else {
2185            stack.pop();
2186            order.extend(by);
2187        }
2188    }
2189    order.reverse();
2190    if order.len() != edges.len() {
2191        return Ok(None);
2192    }
2193    if order.iter().enumerate().all(|(at, &i)| at == i) {
2194        return Ok(None);
2195    }
2196    Ok(Some(order.into_iter().map(|i| edges[i].clone()).collect()))
2197}
2198
2199/// Place a shape by the file's placement number.
2200fn placed(shape: Shape, built: &Built, number: usize) -> OgeomResult<Shape> {
2201    let location = built.location(number)?;
2202    if location.is_identity() {
2203        return Ok(shape);
2204    }
2205    Ok(shape.moved(&location))
2206}
2207
2208/// A word, or a number and the continuity written straight after it.
2209///
2210/// A seam's record runs its second chart number into its continuity with no
2211/// space between (`72CN` for chart 72, continuity `CN`), so the two are
2212/// told apart by the continuity names, the only words that can follow.
2213fn split_continuity(word: &str) -> impl Iterator<Item = &str> {
2214    let split = ["C0", "C1", "C2", "C3", "CN", "G1", "G2"]
2215        .iter()
2216        .find_map(|name| {
2217            let number = word.strip_suffix(name)?;
2218            (!number.is_empty() && number.bytes().all(|b| b.is_ascii_digit()))
2219                .then(|| word.split_at(number.len()))
2220        });
2221    let (first, second) = split.map_or((word, None), |(a, b)| (a, Some(b)));
2222    std::iter::once(first).chain(second)
2223}
2224
2225/// A whitespace-delimited walk over the file's body.
2226struct Cursor<'a> {
2227    tokens: Vec<&'a str>,
2228    at: usize,
2229}
2230
2231impl<'a> Cursor<'a> {
2232    fn new(text: &'a str) -> Self {
2233        Self {
2234            tokens: text.split_whitespace().flat_map(split_continuity).collect(),
2235            at: 0,
2236        }
2237    }
2238
2239    fn word(&mut self) -> OgeomResult<&'a str> {
2240        let Some(token) = self.tokens.get(self.at) else {
2241            ogeom_bail!(Construction, "the file ends in the middle of a record");
2242        };
2243        self.at += 1;
2244        Ok(token)
2245    }
2246
2247    fn peek(&self) -> Option<&'a str> {
2248        self.tokens.get(self.at).copied()
2249    }
2250
2251    fn expect(&mut self, what: &str) -> OgeomResult<()> {
2252        let token = self.word()?;
2253        if token != what {
2254            ogeom_bail!(Construction, "expected the {what} table, found `{token}`");
2255        }
2256        Ok(())
2257    }
2258
2259    fn number(&mut self) -> OgeomResult<f64> {
2260        let token = self.word()?;
2261        let Ok(value) = token.parse::<f64>() else {
2262            ogeom_bail!(Construction, "`{token}` is not a number");
2263        };
2264        Ok(value)
2265    }
2266
2267    fn integer(&mut self) -> OgeomResult<i64> {
2268        let token = self.word()?;
2269        let Ok(value) = token.parse::<i64>() else {
2270            ogeom_bail!(Construction, "`{token}` is not a whole number");
2271        };
2272        Ok(value)
2273    }
2274
2275    /// Room to reserve for `n` items read from here on: never more than the
2276    /// words left, since every item takes at least one, so a count in the
2277    /// file cannot size an allocation past what the file holds.
2278    fn room(&self, n: usize) -> usize {
2279        n.min(self.tokens.len().saturating_sub(self.at))
2280    }
2281
2282    fn count(&mut self) -> OgeomResult<usize> {
2283        let value = self.integer()?;
2284        let Ok(count) = usize::try_from(value) else {
2285            ogeom_bail!(Construction, "{value} is not a count");
2286        };
2287        Ok(count)
2288    }
2289
2290    fn flag(&mut self) -> OgeomResult<bool> {
2291        Ok(self.count()? != 0)
2292    }
2293
2294    fn point(&mut self) -> OgeomResult<Point> {
2295        Ok(Point::new(self.number()?, self.number()?, self.number()?))
2296    }
2297
2298    fn point2(&mut self) -> OgeomResult<Point2> {
2299        Ok(Point2::new(self.number()?, self.number()?))
2300    }
2301
2302    fn direction(&mut self, tol: Tolerances) -> OgeomResult<Direction> {
2303        let v = Vector::new(self.number()?, self.number()?, self.number()?);
2304        Direction::new(v, tol)
2305    }
2306
2307    fn direction2(&mut self, tol: Tolerances) -> OgeomResult<Direction2> {
2308        let v = Vector2::new(self.number()?, self.number()?);
2309        Direction2::new(v, tol)
2310    }
2311
2312    /// The origin-normal-x-y seat every elementary record opens with.
2313    ///
2314    /// A surface's seat may be left-handed, its `y` against the normal
2315    /// crossed with `x`: the chart then runs the other way across the
2316    /// surface, and the side its faces call outward turns with it, so the
2317    /// `y` the file states is kept rather than derived.
2318    fn seat(&mut self, tol: Tolerances) -> OgeomResult<Frame> {
2319        let origin = self.point()?;
2320        let normal = self.direction(tol)?;
2321        let x = self.direction(tol)?;
2322        let y = self.direction(tol)?;
2323        Frame::from_axes(origin, x, y, normal, tol)
2324    }
2325
2326    fn seat2(&mut self, tol: Tolerances) -> OgeomResult<Frame2> {
2327        let origin = self.point2()?;
2328        let x = self.direction2(tol)?;
2329        let y = self.direction2(tol)?;
2330        Frame2::from_axes(origin, x, y, tol)
2331    }
2332
2333    /// Poles and knots, as both spline records spell them.
2334    fn spline_knots(&mut self, degree: usize, count: usize) -> OgeomResult<KnotVector> {
2335        let mut flat = Vec::new();
2336        for _ in 0..count {
2337            let value = self.number()?;
2338            let multiplicity = self.count()?;
2339            // No valid spline repeats a knot more often than its order, and
2340            // no real one is of a degree past 64.
2341            if multiplicity > degree.min(64) + 1 {
2342                ogeom_bail!(
2343                    Construction,
2344                    "a knot of multiplicity {multiplicity} on a degree {degree} spline"
2345                );
2346            }
2347            flat.extend(std::iter::repeat_n(value, multiplicity));
2348        }
2349        KnotVector::new(flat, degree)
2350    }
2351
2352    fn curve(&mut self, tol: Tolerances) -> OgeomResult<Curve> {
2353        let kind = self.count()?;
2354        Ok(match kind {
2355            1 => {
2356                let origin = self.point()?;
2357                let direction = self.direction(tol)?;
2358                // The record carries no window; the edge's own range is what
2359                // bounds it, so the curve is built wide and trimmed there.
2360                LineCurve::over(Axis::new(origin, direction), -OPEN_EXTENT, OPEN_EXTENT)?.into()
2361            }
2362            2 => {
2363                let frame = self.seat(tol)?;
2364                CircleCurve::new(Circle::new(frame, self.number()?, tol)?).into()
2365            }
2366            3 => {
2367                let frame = self.seat(tol)?;
2368                let ellipse = Ellipse::new(frame, self.number()?, self.number()?, tol)?;
2369                EllipseCurve::new(ellipse).into()
2370            }
2371            4 => {
2372                let frame = self.seat(tol)?;
2373                let parabola = Parabola::new(frame, self.number()?, tol)?;
2374                ParabolaCurve::new(parabola, OPEN_EXTENT)?.into()
2375            }
2376            5 => {
2377                let frame = self.seat(tol)?;
2378                let hyperbola = Hyperbola::new(frame, self.number()?, self.number()?, tol)?;
2379                HyperbolaCurve::new(hyperbola, OPEN_EXTENT)?.into()
2380            }
2381            7 => {
2382                let rational = self.flag()?;
2383                let _periodic = self.flag()?;
2384                let degree = self.count()?;
2385                let poles = self.count()?;
2386                let knot_count = self.count()?;
2387                let mut control = Vec::with_capacity(self.room(poles));
2388                for _ in 0..poles {
2389                    let at = self.point()?;
2390                    let weight = if rational { self.number()? } else { 1.0 };
2391                    control.push(Weighted::new(at, weight, tol)?);
2392                }
2393                let knots = self.spline_knots(degree, knot_count)?;
2394                BSplineCurve::rational(knots, control)?.into()
2395            }
2396            8 => {
2397                let lo = self.number()?;
2398                let hi = self.number()?;
2399                let basis = self.curve(tol)?;
2400                Curve::Trimmed(Box::new(TrimmedCurve::new(basis, lo, hi, tol)?))
2401            }
2402            9 => {
2403                let distance = self.number()?;
2404                let reference = self.direction(tol)?;
2405                let basis = self.curve(tol)?;
2406                Curve::Offset(Box::new(ogeom_geom::OffsetCurve::new(
2407                    basis, distance, reference,
2408                )?))
2409            }
2410            other => ogeom_bail!(
2411                Construction,
2412                "curve record {other} is one this does not read; a Bezier \
2413                 curve is record 6, and nothing here builds one"
2414            ),
2415        })
2416    }
2417
2418    fn pcurve(&mut self, tol: Tolerances) -> OgeomResult<PlanarCurve> {
2419        let kind = self.count()?;
2420        Ok(match kind {
2421            1 => {
2422                let origin = self.point2()?;
2423                let direction = self.direction2(tol)?;
2424                Line2d::over(Axis2::new(origin, direction), -OPEN_EXTENT, OPEN_EXTENT)?.into()
2425            }
2426            2 => {
2427                let frame = self.seat2(tol)?;
2428                Circle2d::new(Circle2::new(frame, self.number()?, tol)?).into()
2429            }
2430            3 => {
2431                let frame = self.seat2(tol)?;
2432                let ellipse = Ellipse2::new(frame, self.number()?, self.number()?, tol)?;
2433                Ellipse2d::new(ellipse).into()
2434            }
2435            5 => {
2436                let frame = self.seat2(tol)?;
2437                let hyperbola = Hyperbola2::new(frame, self.number()?, self.number()?, tol)?;
2438                ogeom_geom::Hyperbola2d::over(hyperbola, -OPEN_EXTENT, OPEN_EXTENT)?.into()
2439            }
2440            7 => {
2441                let rational = self.flag()?;
2442                let _periodic = self.flag()?;
2443                let degree = self.count()?;
2444                let poles = self.count()?;
2445                let knot_count = self.count()?;
2446                let mut control = Vec::with_capacity(self.room(poles));
2447                for _ in 0..poles {
2448                    let at = self.point2()?;
2449                    let weight = if rational { self.number()? } else { 1.0 };
2450                    control.push(Weighted::new(at, weight, tol)?);
2451                }
2452                let knots = self.spline_knots(degree, knot_count)?;
2453                ogeom_geom::BSpline2d::rational(knots, control)?.into()
2454            }
2455            8 => {
2456                let lo = self.number()?;
2457                let hi = self.number()?;
2458                let basis = self.pcurve(tol)?;
2459                PlanarCurve::Trimmed(Box::new(ogeom_geom::Trimmed2d::new(basis, lo, hi, tol)?))
2460            }
2461            9 => {
2462                let distance = self.number()?;
2463                let basis = self.pcurve(tol)?;
2464                PlanarCurve::Offset(Box::new(ogeom_geom::Offset2d::new(basis, distance)?))
2465            }
2466            other => ogeom_bail!(
2467                Construction,
2468                "chart curve record {other} is one this does not read"
2469            ),
2470        })
2471    }
2472
2473    fn surface(&mut self, tol: Tolerances) -> OgeomResult<SurfaceGeometry> {
2474        let kind = self.count()?;
2475        // An unbounded carrier needs a window to live in; the format gives
2476        // none, so one wide enough for anything the file can hold is used,
2477        // and the face's own trim is what bounds it in practice.
2478        const REACH: (f64, f64) = (-1e9, 1e9);
2479        Ok(match kind {
2480            1 => {
2481                let frame = self.seat(tol)?;
2482                PlaneSurface::over(Plane::new(frame), REACH, REACH)?.into()
2483            }
2484            2 => {
2485                let frame = self.seat(tol)?;
2486                let cylinder = Cylinder::new(frame, self.number()?, tol)?;
2487                CylinderSurface::new(cylinder, REACH)?.into()
2488            }
2489            3 => {
2490                let frame = self.seat(tol)?;
2491                let cone = Cone::new(frame, self.number()?, self.number()?, tol)?;
2492                ConeSurface::new(cone, REACH)?.into()
2493            }
2494            4 => {
2495                let frame = self.seat(tol)?;
2496                SphereSurface::new(Sphere::new(frame, self.number()?, tol)?).into()
2497            }
2498            5 => {
2499                let frame = self.seat(tol)?;
2500                let torus = Torus::new(frame, self.number()?, self.number()?, tol)?;
2501                TorusSurface::new(torus).into()
2502            }
2503            6 => {
2504                let direction = self.direction(tol)?;
2505                let basis = self.curve(tol)?;
2506                ExtrusionSurface::over(basis, direction, REACH)?.into()
2507            }
2508            7 => {
2509                let origin = self.point()?;
2510                let direction = self.direction(tol)?;
2511                let basis = self.curve(tol)?;
2512                RevolutionSurface::new(basis, Axis::new(origin, direction), core::f64::consts::TAU)?
2513                    .into()
2514            }
2515            9 => {
2516                let u_rational = self.flag()?;
2517                let v_rational = self.flag()?;
2518                let _u_periodic = self.flag()?;
2519                let _v_periodic = self.flag()?;
2520                let u_degree = self.count()?;
2521                let v_degree = self.count()?;
2522                let u_poles = self.count()?;
2523                let v_poles = self.count()?;
2524                let u_knot_count = self.count()?;
2525                let v_knot_count = self.count()?;
2526                let rational = u_rational || v_rational;
2527                let Some(cells) = u_poles.checked_mul(v_poles) else {
2528                    ogeom_bail!(Construction, "a {u_poles} by {v_poles} pole grid");
2529                };
2530                let mut points = Vec::with_capacity(self.room(cells));
2531                for _ in 0..cells {
2532                    let at = self.point()?;
2533                    let weight = if rational { self.number()? } else { 1.0 };
2534                    points.push(Weighted::new(at, weight, tol)?);
2535                }
2536                let u_knots = self.spline_knots(u_degree, u_knot_count)?;
2537                let v_knots = self.spline_knots(v_degree, v_knot_count)?;
2538                let grid = ControlGrid::new(points, u_poles, v_poles)?;
2539                BSplineSurface::rational(u_knots, v_knots, grid)?.into()
2540            }
2541            10 => {
2542                let u = (self.number()?, self.number()?);
2543                let v = (self.number()?, self.number()?);
2544                let basis = self.surface(tol)?;
2545                SurfaceGeometry::Trimmed(Box::new(TrimmedSurface::new(basis, u, v, tol)?))
2546            }
2547            11 => {
2548                let distance = self.number()?;
2549                let basis = self.surface(tol)?;
2550                SurfaceGeometry::Offset(Box::new(ogeom_geom::OffsetSurface::new(basis, distance)?))
2551            }
2552            other => ogeom_bail!(
2553                Construction,
2554                "surface record {other} is one this does not read; a Bezier \
2555                 surface is record 8, and nothing here builds one"
2556            ),
2557        })
2558    }
2559
2560    /// A subshape reference: its orientation, its backward record number and
2561    /// its placement.
2562    fn reference(&mut self) -> OgeomResult<(Orientation, usize, usize)> {
2563        let token = self.word()?;
2564        let (orientation, digits) = match token.split_at(1) {
2565            ("+", rest) => (Orientation::Forward, rest),
2566            ("-", rest) => (Orientation::Reversed, rest),
2567            ("i", rest) => (Orientation::Internal, rest),
2568            ("e", rest) => (Orientation::External, rest),
2569            _ => ogeom_bail!(Construction, "`{token}` does not name a subshape"),
2570        };
2571        let Ok(number) = digits.parse::<usize>() else {
2572            ogeom_bail!(Construction, "`{token}` does not name a subshape number");
2573        };
2574        Ok((orientation, number, self.count()?))
2575    }
2576
2577    fn skip_polygon3d(&mut self) -> OgeomResult<()> {
2578        let nodes = self.count()?;
2579        let has_parameters = self.flag()?;
2580        let _deflection = self.number()?;
2581        for _ in 0..nodes * 3 {
2582            let _ = self.number()?;
2583        }
2584        if has_parameters {
2585            for _ in 0..nodes {
2586                let _ = self.number()?;
2587            }
2588        }
2589        Ok(())
2590    }
2591
2592    fn skip_polygon_on_triangulation(&mut self) -> OgeomResult<()> {
2593        let nodes = self.count()?;
2594        for _ in 0..nodes {
2595            let _ = self.integer()?;
2596        }
2597        // An optional parameter list follows, introduced by `p`.
2598        if self.peek() == Some("p") {
2599            let _ = self.word()?;
2600            let _deflection = self.number()?;
2601            let _flag = self.count()?;
2602            let count = self.count()?;
2603            for _ in 0..count {
2604                let _ = self.number()?;
2605            }
2606        }
2607        Ok(())
2608    }
2609
2610    fn skip_triangulation(&mut self) -> OgeomResult<()> {
2611        let nodes = self.count()?;
2612        let triangles = self.count()?;
2613        let has_parameters = self.flag()?;
2614        let _deflection = self.number()?;
2615        for _ in 0..nodes * 3 {
2616            let _ = self.number()?;
2617        }
2618        if has_parameters {
2619            for _ in 0..nodes * 2 {
2620                let _ = self.number()?;
2621            }
2622        }
2623        for _ in 0..triangles * 3 {
2624            let _ = self.integer()?;
2625        }
2626        Ok(())
2627    }
2628
2629    /// One topology record, built into the model against what came before.
2630    fn record(
2631        &mut self,
2632        model: &mut Model,
2633        built: &mut Built,
2634        shapes: &[Shape],
2635        total: usize,
2636        tol: Tolerances,
2637    ) -> OgeomResult<Shape> {
2638        let kind = self.word()?;
2639        let data = match kind {
2640            "Ve" => {
2641                let tolerance = self.number()?;
2642                let at = self.point()?;
2643                // The parameter list, which this recovers by projection where
2644                // it needs it rather than trusting a file's copy.
2645                loop {
2646                    let first = self.word()?;
2647                    let second = self.word()?;
2648                    if first == "0" && second == "0" {
2649                        break;
2650                    }
2651                    // A representation: its data depends on the kind in
2652                    // `second`, and every one ends with a placement number.
2653                    match second {
2654                        "1" => {
2655                            let _ = self.count()?;
2656                        }
2657                        "2" | "3" => {
2658                            let _ = self.count()?;
2659                            let _ = self.count()?;
2660                        }
2661                        other => ogeom_bail!(
2662                            Construction,
2663                            "a vertex representation of kind {other} is not one this reads"
2664                        ),
2665                    }
2666                    let _ = self.count()?;
2667                }
2668                Record::Vertex(VertexData {
2669                    point: at,
2670                    tolerance: Tolerance::new(tolerance.max(tol.confusion()))?,
2671                })
2672            }
2673            "Ed" => {
2674                let tolerance = self.number()?;
2675                let _same_parameter = self.flag()?;
2676                let _same_range = self.flag()?;
2677                let degenerate = self.flag()?;
2678                let mut data = EdgeData::new();
2679                data.tolerance = Tolerance::new(tolerance.max(tol.confusion()))?;
2680                data.degenerate = degenerate;
2681                let mut charts = Vec::new();
2682                loop {
2683                    let kind = self.count()?;
2684                    match kind {
2685                        0 => break,
2686                        1 => {
2687                            let curve = self.count()?;
2688                            let location = self.count()?;
2689                            let lo = self.number()?;
2690                            let hi = self.number()?;
2691                            let id = built.curve(model, curve, location, tol)?;
2692                            data.add(EdgeRepr::Curve3d {
2693                                curve: id,
2694                                range: (lo, hi),
2695                                location: Location::identity(),
2696                            });
2697                        }
2698                        2 | 3 => {
2699                            let first = self.count()?;
2700                            let second = if kind == 3 {
2701                                let reversed = self.count()?;
2702                                let _continuity = self.word()?;
2703                                Some(reversed)
2704                            } else {
2705                                None
2706                            };
2707                            let surface = self.count()?;
2708                            let location = self.count()?;
2709                            let lo = self.number()?;
2710                            let hi = self.number()?;
2711                            let chart = |number: usize| {
2712                                number
2713                                    .checked_sub(1)
2714                                    .and_then(|at| built.pcurves.get(at))
2715                                    .copied()
2716                            };
2717                            let (Some(curve), true, true) = (
2718                                chart(first),
2719                                second.is_none_or(|n| chart(n).is_some()),
2720                                (1..=built.surfaces.len()).contains(&surface),
2721                            ) else {
2722                                ogeom_bail!(
2723                                    Construction,
2724                                    "an edge names chart curve {first} on surface {surface}"
2725                                );
2726                            };
2727                            charts.push(StatedChart {
2728                                curves: (curve, second.and_then(chart)),
2729                                surface,
2730                                location,
2731                                range: (lo, hi),
2732                            });
2733                        }
2734                        4 => {
2735                            let _continuity = self.word()?;
2736                            for _ in 0..4 {
2737                                let _ = self.count()?;
2738                            }
2739                        }
2740                        5 => {
2741                            let _ = self.count()?;
2742                            let _ = self.count()?;
2743                        }
2744                        6 => {
2745                            for _ in 0..3 {
2746                                let _ = self.count()?;
2747                            }
2748                        }
2749                        7 => {
2750                            for _ in 0..4 {
2751                                let _ = self.count()?;
2752                            }
2753                        }
2754                        other => ogeom_bail!(
2755                            Construction,
2756                            "an edge representation of kind {other} is not one this reads"
2757                        ),
2758                    }
2759                }
2760                // The file states whether its representations agree on
2761                // parameterization. That is the writer's claim about its own
2762                // data, and it is checked here rather than believed: once the
2763                // faces have bound the edge's charts, the representations are
2764                // evaluated against each other, and the claim is only
2765                // re-established when they actually agree.
2766                data.assert_same_parameter(agree_on_parameter(model, &data, tol));
2767                Record::Edge(Box::new(data), charts)
2768            }
2769            "Fa" => {
2770                let _natural = self.flag()?;
2771                let tolerance = self.number()?;
2772                let surface = self.count()?;
2773                let location = self.count()?;
2774                if self.peek() == Some("2") {
2775                    let _ = self.word()?;
2776                    let _triangulation = self.count()?;
2777                }
2778                let on = built.surface(model, surface.max(1), location, tol)?;
2779                Record::Face(
2780                    Box::new(FaceData {
2781                        surface: on,
2782                        location: Location::identity(),
2783                        tolerance: Tolerance::new(tolerance.max(tol.confusion()))?,
2784                        natural_restriction: false,
2785                        triangulation: None,
2786                    }),
2787                    (surface.max(1), location),
2788                )
2789            }
2790            "Wi" => Record::Container(ShapeType::Wire),
2791            "Sh" => Record::Container(ShapeType::Shell),
2792            "So" => Record::Container(ShapeType::Solid),
2793            "CS" => Record::Container(ShapeType::CompSolid),
2794            "Co" => Record::Container(ShapeType::Compound),
2795            other => ogeom_bail!(Construction, "`{other}` is not a shape record this reads"),
2796        };
2797
2798        // The flag word, which says nothing about the shape, then the
2799        // children.
2800        let _flags = self.word()?;
2801        let mut children = Vec::new();
2802        let mut written = Vec::new();
2803        while self.peek() != Some("*") {
2804            let (orientation, number, location) = self.reference()?;
2805            written.push((number, location));
2806            let Some(child) = pick(shapes, number, total) else {
2807                ogeom_bail!(
2808                    Construction,
2809                    "a record names subshape {number}, which is not above it"
2810                );
2811            };
2812            let sense = built.sense(&child, orientation);
2813            children.push(placed(child, built, location)?.composed(sense));
2814        }
2815        let _ = self.word()?;
2816        built.written.push(written);
2817
2818        Ok(match data {
2819            Record::Vertex(data) => model.add_vertex(data),
2820            Record::Edge(data, charts) => {
2821                // The file may list an edge's end before its start; an edge
2822                // here holds its start first.
2823                children.sort_by_key(|v| v.orientation() != Orientation::Forward);
2824                let edge = model.add_edge(*data, &children)?;
2825                if !charts.is_empty() {
2826                    built.charts.insert(edge.node(), charts);
2827                }
2828                edge
2829            }
2830            Record::Face(data, stated) => {
2831                // A wire the file places inside its face is rebuilt in the
2832                // face's own frame, its edges carrying the placement: what
2833                // reads a face's boundary takes the wire's frame for the
2834                // face's.
2835                let mut wires = Vec::with_capacity(children.len());
2836                for wire in children {
2837                    if wire.location().is_identity() {
2838                        wires.push(wire);
2839                        continue;
2840                    }
2841                    let edges = model.children_of(&wire.oriented(Orientation::Forward))?;
2842                    wires.push(model.add_wire(&edges)?.oriented(wire.orientation()));
2843                }
2844                built.bind_charts(model, stated, data.surface, &wires, tol)?;
2845                if built.turned(data.surface) {
2846                    let wires: Vec<Shape> = wires.iter().map(Shape::reversed).collect();
2847                    let face = model.add_face(*data, &wires)?;
2848                    built.turned_faces.insert(face.node());
2849                    face
2850                } else {
2851                    model.add_face(*data, &wires)?
2852                }
2853            }
2854            Record::Container(ShapeType::Wire) => {
2855                let walk = walked(model, &children)?;
2856                model.add_wire(walk.as_deref().unwrap_or(&children))?
2857            }
2858            Record::Container(ShapeType::Shell) => {
2859                let faces = children
2860                    .into_iter()
2861                    .map(|face| settled(model, built, face, tol))
2862                    .collect::<OgeomResult<Vec<_>>>()?;
2863                model.add_shell(&faces)?
2864            }
2865            Record::Container(ShapeType::Solid) => model.add_solid(&children)?,
2866            Record::Container(ShapeType::CompSolid) => model.add_compsolid(&children)?,
2867            Record::Container(_) => model.add_compound(&children)?,
2868        })
2869    }
2870}
2871
2872/// What a record turned out to be, before its children are known.
2873enum Record {
2874    Vertex(VertexData),
2875    /// The edge, with the charts it states for the faces to bind.
2876    Edge(Box<EdgeData>, Vec<StatedChart>),
2877    /// The face, with its surface and placement as the file numbers them.
2878    Face(Box<FaceData>, (usize, usize)),
2879    Container(ShapeType),
2880}
2881
2882/// Whether an edge's representations land on the same point at the same
2883/// parameter, which is what the same-parameter claim means.
2884///
2885/// Sampled: nine stations across the edge's own range, each read through
2886/// every representation it carries, all held to the edge's stated tolerance.
2887/// A file whose pcurve was fitted independently of its curve fails this, and
2888/// it should: the claim is what every algorithm downstream relies on.
2889fn agree_on_parameter(model: &Model, data: &EdgeData, tol: Tolerances) -> bool {
2890    use ogeom_geom::Surface as _;
2891    let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
2892        return false;
2893    };
2894    let Some(geometry) = model.geometry().curve(*curve) else {
2895        return false;
2896    };
2897    let allowed = data.tolerance.get().max(tol.confusion());
2898    for representation in &data.representations {
2899        // A chart bound to the edge's use at a placement follows the curve
2900        // carried there.
2901        let Ok(moved) = representation
2902            .location()
2903            .map_or(Ok(Transform::IDENTITY), |l| l.composed(model.datums()))
2904        else {
2905            return false;
2906        };
2907        let (pcurve, prange, surface) = match representation {
2908            EdgeRepr::PCurve {
2909                curve,
2910                range,
2911                surface,
2912                ..
2913            } => (*curve, *range, *surface),
2914            EdgeRepr::Seam {
2915                forward,
2916                range,
2917                surface,
2918                ..
2919            } => (*forward, *range, *surface),
2920            _ => continue,
2921        };
2922        let (Some(chart), Some(on)) = (
2923            model.geometry().pcurve(pcurve),
2924            model.geometry().surface(surface),
2925        ) else {
2926            return false;
2927        };
2928        for k in 0..=8 {
2929            let f = f64::from(k) / 8.0;
2930            let t = (range.1 - range.0).mul_add(f, range.0);
2931            let pt = (prange.1 - prange.0).mul_add(f, prange.0);
2932            let (Ok(at), Ok(uv)) = (geometry.point_at(t, tol), chart.point_at(pt, tol)) else {
2933                return false;
2934            };
2935            let Ok(lifted) = on.point_at(uv.x, uv.y, tol) else {
2936                return false;
2937            };
2938            if lifted.distance(moved.apply(at)) > allowed {
2939                return false;
2940            }
2941        }
2942    }
2943    true
2944}