Skip to main content

geop_ops_step/
export.rs

1//! Writing a part as a STEP file (AP214, the geometry every CAD system
2//! reads): each solid a `MANIFOLD_SOLID_BREP` — a `BREP_WITH_VOIDS` if it
3//! has voids — and each sheet of faces standing on their own a
4//! `SHELL_BASED_SURFACE_MODEL`, with every surface and curve written exactly
5//! as the kernel holds it: a `B_SPLINE_*_WITH_KNOTS`, rational where a
6//! weight is not one. Lengths are millimetres: a length of one in the
7//! kernel is one millimetre.
8//!
9//! A part placing others ([`write_part`]) is written as an assembly: each
10//! distinct component placed in it — every instance sharing one built
11//! component — a product of its own with its bodies in its own frame, and
12//! each placement a `NEXT_ASSEMBLY_USAGE_OCCURRENCE` of it in the product
13//! it is placed in, with an `ITEM_DEFINED_TRANSFORMATION` saying where. A
14//! part placing others has a `SHAPE_REPRESENTATION` of the placements, its
15//! own bodies related to it; a part placing none has its bodies'
16//! representation as its shape.
17//!
18//! Every value is written as its interval's midpoint. Entities are named
19//! after the part's names for them, so a face picked as `extrude(E,end)`
20//! here is called that in the file.
21
22use geop_core_program::reader::Reader;
23use std::collections::HashMap;
24
25use geop_core_editor::output::{Content, Export, Exporter, ExporterInfo, NoOptions, Output};
26use geop_core_geometry::{nurb_curve::NurbCurve3D, nurb_surface::NurbSurface3D, search};
27use geop_core_math::{
28    geop_error::{GeopError, GeopResult},
29    primitives::Pose,
30    scalars::Scalar,
31    vector::Vector3,
32};
33use geop_core_topology::{
34    CoedgeGeometry, EdgeId, FaceId, Model, Sense, ShellId, VertexId, boundary::BoundaryType,
35    contains::shell::solid_lumps,
36};
37use geop_ops_model::parameters::{read_properties, stem};
38use geop_ops_model::placed::placed_parts;
39use geop_ops_model::{NamedModel, RefId, read_model};
40
41use crate::part21::{Exchange, Instance, Record, Value};
42
43/// Fixed seed for the rays telling a solid's lumps from its voids: the
44/// answer does not depend on it, and a constant keeps exports alike.
45const LUMPS_SEED: u64 = 0x05EE_D0F1_A3B5;
46
47/// The uncertainty the file declares for its lengths, in millimetres: how
48/// far apart two points may be and still be read as one. Far looser than
49/// anything the kernel holds, which is what a reader needs to rebuild the
50/// topology — far tighter than any feature.
51const UNCERTAINTY: f64 = 1e-7;
52
53/// The bodies of `model` as the text of a STEP file whose product is
54/// called `name`.
55pub fn write_step(model: &NamedModel, name: &str) -> GeopResult<String> {
56    let mut writer = Writer::default();
57    let context = writer.context();
58    writer.part(model, &[], name, &context)?;
59    Ok(writer.file(name))
60}
61
62/// The part `reader` reads — its bodies and the parts placed in it, however
63/// deep — as the text of a STEP file whose product is called `name`.
64pub fn write_part(reader: &Reader, name: &str) -> GeopResult<String> {
65    let mut writer = Writer::default();
66    let context = writer.context();
67    writer.assembly(reader, name, &context)?;
68    Ok(writer.file(name))
69}
70
71/// A product written: what an assembly placing it refers to.
72#[derive(Clone, Copy)]
73struct Written {
74    /// Its `PRODUCT_DEFINITION`.
75    definition: u64,
76    /// The representation of its shape.
77    shape: u64,
78    /// The placement in `shape` its own frame is.
79    origin: u64,
80}
81
82impl Writer {
83    /// What it has written, as the text of a STEP file whose product is
84    /// called `name`.
85    fn file(self, name: &str) -> String {
86        let file_name = format!("{name}.step");
87        Exchange {
88            header: vec![
89                Record {
90                    name: "FILE_DESCRIPTION".into(),
91                    args: vec![Value::List(vec![string(name)]), string("2;1")],
92                },
93                Record {
94                    name: "FILE_NAME".into(),
95                    args: vec![
96                        string(&file_name),
97                        string(""),
98                        Value::List(vec![string("")]),
99                        Value::List(vec![string("")]),
100                        string("geop"),
101                        string("geop"),
102                        string(""),
103                    ],
104                },
105                Record {
106                    name: "FILE_SCHEMA".into(),
107                    args: vec![Value::List(vec![string(
108                        "AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }",
109                    )])],
110                },
111            ],
112            instances: self.instances,
113        }
114        .write()
115    }
116}
117
118fn string(s: &str) -> Value {
119    Value::String(s.to_string())
120}
121
122fn logical(b: bool) -> Value {
123    Value::Enum(if b { "T" } else { "F" }.into())
124}
125
126fn reals(xs: impl IntoIterator<Item = f64>) -> Value {
127    Value::List(xs.into_iter().map(Value::Real).collect())
128}
129
130fn refs(ids: impl IntoIterator<Item = u64>) -> Value {
131    Value::List(ids.into_iter().map(Value::Ref).collect())
132}
133
134/// The distinct knots of `knots` and how often each is repeated, as STEP
135/// writes a knot vector. Knots are repeated when the kernel built them as
136/// the same value, so they are grouped by equal midpoints.
137fn knots_and_multiplicities(knots: &[Scalar]) -> (Value, Value) {
138    let mut distinct: Vec<f64> = Vec::new();
139    let mut counts: Vec<i64> = Vec::new();
140    for k in knots {
141        let k = k.to_f64();
142        if distinct.last() == Some(&k) {
143            *counts.last_mut().expect("a knot was pushed with it") += 1;
144        } else {
145            distinct.push(k);
146            counts.push(1);
147        }
148    }
149    (
150        Value::List(counts.into_iter().map(Value::Integer).collect()),
151        reals(distinct),
152    )
153}
154
155/// The shared entities every representation refers to.
156struct Context {
157    application: u64,
158    representation: u64,
159}
160
161#[derive(Default)]
162struct Writer {
163    instances: std::collections::BTreeMap<u64, Instance>,
164}
165
166impl Writer {
167    fn insert(&mut self, instance: Instance) -> u64 {
168        let id = self.instances.len() as u64 + 1;
169        self.instances.insert(id, instance);
170        id
171    }
172
173    fn add(&mut self, name: &str, args: Vec<Value>) -> u64 {
174        self.insert(Instance::Simple(Record {
175            name: name.into(),
176            args,
177        }))
178    }
179
180    /// A complex instance of the records `records`, written in alphabetical
181    /// order as Part 21 requires.
182    fn add_complex(&mut self, mut records: Vec<Record>) -> u64 {
183        records.sort_by(|a, b| a.name.cmp(&b.name));
184        self.insert(Instance::Complex(records))
185    }
186
187    fn point(&mut self, p: [f64; 3]) -> u64 {
188        self.add("CARTESIAN_POINT", vec![string(""), reals(p)])
189    }
190
191    fn placement(&mut self, origin: [f64; 3], axis: [f64; 3], reference: [f64; 3]) -> u64 {
192        let origin = self.point(origin);
193        let axis = self.add("DIRECTION", vec![string(""), reals(axis)]);
194        let reference = self.add("DIRECTION", vec![string(""), reals(reference)]);
195        self.add(
196            "AXIS2_PLACEMENT_3D",
197            vec![
198                string(""),
199                Value::Ref(origin),
200                Value::Ref(axis),
201                Value::Ref(reference),
202            ],
203        )
204    }
205
206    /// The application context, the units — millimetres, radians — and the
207    /// geometric context with the file's uncertainty.
208    fn context(&mut self) -> Context {
209        let application = self.add(
210            "APPLICATION_CONTEXT",
211            vec![string(
212                "core data for automotive mechanical design processes",
213            )],
214        );
215        self.add(
216            "APPLICATION_PROTOCOL_DEFINITION",
217            vec![
218                string("international standard"),
219                string("automotive_design"),
220                Value::Integer(2000),
221                Value::Ref(application),
222            ],
223        );
224        let record = |name: &str, args: Vec<Value>| Record {
225            name: name.into(),
226            args,
227        };
228        let length_unit = self.add_complex(vec![
229            record("LENGTH_UNIT", vec![]),
230            record("NAMED_UNIT", vec![Value::Derived]),
231            record(
232                "SI_UNIT",
233                vec![Value::Enum("MILLI".into()), Value::Enum("METRE".into())],
234            ),
235        ]);
236        let angle_unit = self.add_complex(vec![
237            record("NAMED_UNIT", vec![Value::Derived]),
238            record("PLANE_ANGLE_UNIT", vec![]),
239            record("SI_UNIT", vec![Value::Null, Value::Enum("RADIAN".into())]),
240        ]);
241        let solid_angle_unit = self.add_complex(vec![
242            record("NAMED_UNIT", vec![Value::Derived]),
243            record(
244                "SI_UNIT",
245                vec![Value::Null, Value::Enum("STERADIAN".into())],
246            ),
247            record("SOLID_ANGLE_UNIT", vec![]),
248        ]);
249        let uncertainty = self.add(
250            "UNCERTAINTY_MEASURE_WITH_UNIT",
251            vec![
252                Value::Typed("LENGTH_MEASURE".into(), Box::new(Value::Real(UNCERTAINTY))),
253                Value::Ref(length_unit),
254                string("distance_accuracy_value"),
255                string("confusion accuracy"),
256            ],
257        );
258        let representation = self.add_complex(vec![
259            record("GEOMETRIC_REPRESENTATION_CONTEXT", vec![Value::Integer(3)]),
260            record(
261                "GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT",
262                vec![refs([uncertainty])],
263            ),
264            record(
265                "GLOBAL_UNIT_ASSIGNED_CONTEXT",
266                vec![refs([length_unit, angle_unit, solid_angle_unit])],
267            ),
268            record("REPRESENTATION_CONTEXT", vec![string("geop"), string("3D")]),
269        ]);
270        Context {
271            application,
272            representation,
273        }
274    }
275
276    /// The product `name` and its definition, down to the
277    /// `PRODUCT_DEFINITION_SHAPE` its shape representation is attached to:
278    /// the `PRODUCT_DEFINITION` and that.
279    fn product(&mut self, name: &str, application: u64) -> (u64, u64) {
280        let context = self.add(
281            "PRODUCT_CONTEXT",
282            vec![string(""), Value::Ref(application), string("mechanical")],
283        );
284        let product = self.add(
285            "PRODUCT",
286            vec![string(name), string(name), string(""), refs([context])],
287        );
288        self.add(
289            "PRODUCT_RELATED_PRODUCT_CATEGORY",
290            vec![string("part"), Value::Null, refs([product])],
291        );
292        let formation = self.add(
293            "PRODUCT_DEFINITION_FORMATION",
294            vec![string(""), string(""), Value::Ref(product)],
295        );
296        let definition_context = self.add(
297            "PRODUCT_DEFINITION_CONTEXT",
298            vec![
299                string("part definition"),
300                Value::Ref(application),
301                string("design"),
302            ],
303        );
304        let definition = self.add(
305            "PRODUCT_DEFINITION",
306            vec![
307                string("design"),
308                string(""),
309                Value::Ref(formation),
310                Value::Ref(definition_context),
311            ],
312        );
313        let shape = self.add(
314            "PRODUCT_DEFINITION_SHAPE",
315            vec![string(""), string(""), Value::Ref(definition)],
316        );
317        (definition, shape)
318    }
319
320    /// The placement the pose `pose` moves the frame of what it places to.
321    fn pose(&mut self, pose: &Pose) -> GeopResult<u64> {
322        let plain = |q: Vector3| q.to_f64_array();
323        // Where its `x` and `z` turn to.
324        let [x, _, z] = pose.rotation().rotation_columns()?;
325        Ok(self.placement(plain(pose.position()), plain(z), plain(x)))
326    }
327
328    /// The part `reader` reads as the product `name`, and each part placed
329    /// in it as a product of its own. A product's placements are where its
330    /// own part puts them.
331    ///
332    /// TODO: Later, with the parts of no state shared between instances
333    /// (see [`geop_core_program::part::Part`]), write each part
334    /// shared once, however often it is placed.
335    fn assembly(&mut self, reader: &Reader, name: &str, context: &Context) -> GeopResult<Written> {
336        let ctx = |e: GeopError| e.with_context(format!("writing the part {name:?} as STEP"));
337        let mut placed = Vec::new();
338        let parts = placed_parts(reader).map_err(ctx)?;
339        for part in parts.iter().filter(|p| p.parent.is_none()) {
340            // Its designation (`ISO 4762 M4x12`), else what it is listed
341            // as: its title, else its file's stem.
342            let inner = part.located.part.clone();
343            let properties = read_properties(&inner).map_err(ctx)?;
344            let stem = stem(&part.located.instance.file);
345            let name = (properties.designation.clone())
346                .unwrap_or_else(|| properties.name(stem).to_string());
347            let child = self.assembly(&inner, &name, context)?;
348            placed.push((part.name.clone(), child, part.located.local));
349        }
350        let model = read_model(reader).map_err(ctx)?;
351        self.part(model.as_ref(), &placed, name, context)
352    }
353
354    /// The bodies of `model`, placing the products `placed` — each by its
355    /// instance's name, where its pose puts it — as the product `name`:
356    /// its shape the representation of its solids if it places nothing,
357    /// else of the placements, its bodies related to it.
358    fn part(
359        &mut self,
360        model: &NamedModel,
361        placed: &[(String, Written, Pose)],
362        name: &str,
363        context: &Context,
364    ) -> GeopResult<Written> {
365        let mut solids = Vec::new();
366        let mut sheets = Vec::new();
367        self.bodies(model, &mut solids, &mut sheets)?;
368        let (definition, product_shape) = self.product(name, context.application);
369        let origin = self.placement([0.0; 3], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0]);
370        let representation = |writer: &mut Self, kind: &str, items: &[u64]| {
371            let mut all = vec![Value::Ref(origin)];
372            all.extend(items.iter().map(|&id| Value::Ref(id)));
373            writer.add(
374                kind,
375                vec![
376                    string(name),
377                    Value::List(all),
378                    Value::Ref(context.representation),
379                ],
380            )
381        };
382        let relate = |writer: &mut Self, a: u64, b: u64| {
383            writer.add(
384                "SHAPE_REPRESENTATION_RELATIONSHIP",
385                vec![string(""), string(""), Value::Ref(a), Value::Ref(b)],
386            );
387        };
388        let brep = (placed.is_empty() || !solids.is_empty())
389            .then(|| representation(self, "ADVANCED_BREP_SHAPE_REPRESENTATION", &solids));
390        let surfaces = (!sheets.is_empty())
391            .then(|| representation(self, "MANIFOLD_SURFACE_SHAPE_REPRESENTATION", &sheets));
392        let axes: Vec<u64> = (placed.iter())
393            .map(|(_, _, pose)| self.pose(pose))
394            .collect::<GeopResult<_>>()?;
395        // A part placing none is its bodies; one placing others is the
396        // placements, its bodies related to them.
397        let shape = match brep {
398            Some(brep) if placed.is_empty() => brep,
399            _ => {
400                let shape = representation(self, "SHAPE_REPRESENTATION", &axes);
401                if let Some(brep) = brep {
402                    relate(self, shape, brep);
403                }
404                shape
405            }
406        };
407        if let Some(surfaces) = surfaces {
408            relate(self, shape, surfaces);
409        }
410        self.add(
411            "SHAPE_DEFINITION_REPRESENTATION",
412            vec![Value::Ref(product_shape), Value::Ref(shape)],
413        );
414
415        for ((instance, child, _), axis) in placed.iter().zip(axes) {
416            let occurrence = self.add(
417                "NEXT_ASSEMBLY_USAGE_OCCURRENCE",
418                vec![
419                    string(instance),
420                    string(instance),
421                    string(""),
422                    Value::Ref(definition),
423                    Value::Ref(child.definition),
424                    Value::Null,
425                ],
426            );
427            let occurrence_shape = self.add(
428                "PRODUCT_DEFINITION_SHAPE",
429                vec![string(""), string(""), Value::Ref(occurrence)],
430            );
431            let transformation = self.add(
432                "ITEM_DEFINED_TRANSFORMATION",
433                vec![
434                    string(""),
435                    string(""),
436                    Value::Ref(child.origin),
437                    Value::Ref(axis),
438                ],
439            );
440            let record = |name: &str, args: Vec<Value>| Record {
441                name: name.into(),
442                args,
443            };
444            let relationship = self.add_complex(vec![
445                record(
446                    "REPRESENTATION_RELATIONSHIP",
447                    vec![
448                        string(""),
449                        string(""),
450                        Value::Ref(child.shape),
451                        Value::Ref(shape),
452                    ],
453                ),
454                record(
455                    "REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION",
456                    vec![Value::Ref(transformation)],
457                ),
458                record("SHAPE_REPRESENTATION_RELATIONSHIP", vec![]),
459            ]);
460            self.add(
461                "CONTEXT_DEPENDENT_SHAPE_REPRESENTATION",
462                vec![Value::Ref(relationship), Value::Ref(occurrence_shape)],
463            );
464        }
465        Ok(Written {
466            definition,
467            shape,
468            origin,
469        })
470    }
471
472    /// Every body of `named`, in its own frame: solids onto `solids`,
473    /// sheets onto `sheets`. Entities are named as `named` names them.
474    fn bodies(
475        &mut self,
476        named: &NamedModel,
477        solids: &mut Vec<u64>,
478        sheets: &mut Vec<u64>,
479    ) -> GeopResult<()> {
480        let model = named.topology();
481        let mut body = BodyWriter {
482            writer: self,
483            model,
484            name: |id: RefId| named.name_of(id).unwrap_or_default().to_string(),
485            vertices: HashMap::new(),
486            edges: HashMap::new(),
487        };
488        // The arenas are ordered by id, so these come oldest first.
489        for &solid_id in model.solids.keys() {
490            let name = (body.name)(solid_id.into());
491            // A solid of several lumps — a boolean that cut a bar in two —
492            // is a brep for each, each with the voids in it.
493            let lumps = solid_lumps(
494                model,
495                solid_id,
496                search::MAX_NODES,
497                search::min_subdivision_size(),
498                LUMPS_SEED,
499            )?;
500            for (outer, voids) in lumps {
501                let outer = body.shell(outer, "CLOSED_SHELL", false)?;
502                let id = if voids.is_empty() {
503                    body.writer.add(
504                        "MANIFOLD_SOLID_BREP",
505                        vec![string(&name), Value::Ref(outer)],
506                    )
507                } else {
508                    // A void's faces point into it, out of the material: the
509                    // closed shell around the void is them turned around, and
510                    // the void is that shell, oriented back.
511                    let mut oriented = Vec::new();
512                    for void in voids {
513                        let shell = body.shell(void, "CLOSED_SHELL", true)?;
514                        oriented.push(body.writer.add(
515                            "ORIENTED_CLOSED_SHELL",
516                            vec![
517                                string(""),
518                                Value::Derived,
519                                Value::Ref(shell),
520                                logical(false),
521                            ],
522                        ));
523                    }
524                    body.writer.add(
525                        "BREP_WITH_VOIDS",
526                        vec![string(&name), Value::Ref(outer), refs(oriented)],
527                    )
528                };
529                solids.push(id);
530            }
531        }
532        let sheet_ids = model
533            .shells
534            .iter()
535            .filter(|(_, shell)| shell.solid.is_none())
536            .map(|(&id, _)| id);
537        for sheet in sheet_ids {
538            let shell = body.shell(sheet, "OPEN_SHELL", false)?;
539            sheets.push(
540                body.writer
541                    .add("SHELL_BASED_SURFACE_MODEL", vec![string(""), refs([shell])]),
542            );
543        }
544        Ok(())
545    }
546}
547
548/// Writes the bodies of one model, sharing each vertex and edge between
549/// the faces that use it.
550struct BodyWriter<'w, 'm, N> {
551    writer: &'w mut Writer,
552    model: &'m Model,
553    name: N,
554    vertices: HashMap<VertexId, u64>,
555    edges: HashMap<EdgeId, u64>,
556}
557
558impl<N: Fn(RefId) -> String> BodyWriter<'_, '_, N> {
559    fn coordinates(p: &Vector3) -> [f64; 3] {
560        p.to_f64_array()
561    }
562
563    /// The homogeneous control point `cp` as STEP writes it: the point,
564    /// and its weight.
565    fn control_point(&mut self, cp: &geop_core_math::vector::Vector4) -> GeopResult<(u64, f64)> {
566        let p = Self::coordinates(&cp.dehomogenized()?);
567        Ok((self.writer.point(p), cp[3].to_f64()))
568    }
569
570    fn vertex(&mut self, id: VertexId) -> GeopResult<u64> {
571        if let Some(&step) = self.vertices.get(&id) {
572            return Ok(step);
573        }
574        let p = Self::coordinates(&self.model.get_vertex(id)?.point);
575        let point = self.writer.point(p);
576        let step = self.writer.add(
577            "VERTEX_POINT",
578            vec![string(&(self.name)(id.into())), Value::Ref(point)],
579        );
580        self.vertices.insert(id, step);
581        Ok(step)
582    }
583
584    fn curve(&mut self, curve: &NurbCurve3D) -> GeopResult<u64> {
585        let mut points = Vec::new();
586        let mut weights = Vec::new();
587        for cp in &curve.control_points {
588            let (point, weight) = self.control_point(cp)?;
589            points.push(point);
590            weights.push(weight);
591        }
592        let (multiplicities, knots) = knots_and_multiplicities(&curve.knot_vector);
593        let degree = Value::Integer(curve.degree as i64);
594        let form = Value::Enum("UNSPECIFIED".into());
595        if weights.iter().all(|&w| w == 1.0) {
596            return Ok(self.writer.add(
597                "B_SPLINE_CURVE_WITH_KNOTS",
598                vec![
599                    string(""),
600                    degree,
601                    refs(points),
602                    form.clone(),
603                    logical(false),
604                    logical(false),
605                    multiplicities,
606                    knots,
607                    form,
608                ],
609            ));
610        }
611        let record = |name: &str, args: Vec<Value>| Record {
612            name: name.into(),
613            args,
614        };
615        Ok(self.writer.add_complex(vec![
616            record("BOUNDED_CURVE", vec![]),
617            record(
618                "B_SPLINE_CURVE",
619                vec![
620                    degree,
621                    refs(points),
622                    form.clone(),
623                    logical(false),
624                    logical(false),
625                ],
626            ),
627            record(
628                "B_SPLINE_CURVE_WITH_KNOTS",
629                vec![multiplicities, knots, form],
630            ),
631            record("CURVE", vec![]),
632            record("GEOMETRIC_REPRESENTATION_ITEM", vec![]),
633            record("RATIONAL_B_SPLINE_CURVE", vec![reals(weights)]),
634            record("REPRESENTATION_ITEM", vec![string("")]),
635        ]))
636    }
637
638    fn surface(&mut self, surface: &NurbSurface3D) -> GeopResult<u64> {
639        let mut rows = Vec::new();
640        let mut weight_rows = Vec::new();
641        for i in 0..surface.num_u {
642            let mut row = Vec::new();
643            let mut weights = Vec::new();
644            for j in 0..surface.num_v {
645                let (point, weight) =
646                    self.control_point(&surface.control_points[i * surface.num_v + j])?;
647                row.push(point);
648                weights.push(weight);
649            }
650            rows.push(refs(row));
651            weight_rows.push(weights);
652        }
653        let (u_multiplicities, u_knots) = knots_and_multiplicities(&surface.knot_vector_u);
654        let (v_multiplicities, v_knots) = knots_and_multiplicities(&surface.knot_vector_v);
655        let degree_u = Value::Integer(surface.degree_u as i64);
656        let degree_v = Value::Integer(surface.degree_v as i64);
657        let form = Value::Enum("UNSPECIFIED".into());
658        if weight_rows.iter().flatten().all(|&w| w == 1.0) {
659            return Ok(self.writer.add(
660                "B_SPLINE_SURFACE_WITH_KNOTS",
661                vec![
662                    string(""),
663                    degree_u,
664                    degree_v,
665                    Value::List(rows),
666                    form.clone(),
667                    logical(false),
668                    logical(false),
669                    logical(false),
670                    u_multiplicities,
671                    v_multiplicities,
672                    u_knots,
673                    v_knots,
674                    form,
675                ],
676            ));
677        }
678        let record = |name: &str, args: Vec<Value>| Record {
679            name: name.into(),
680            args,
681        };
682        Ok(self.writer.add_complex(vec![
683            record("BOUNDED_SURFACE", vec![]),
684            record(
685                "B_SPLINE_SURFACE",
686                vec![
687                    degree_u,
688                    degree_v,
689                    Value::List(rows),
690                    form.clone(),
691                    logical(false),
692                    logical(false),
693                    logical(false),
694                ],
695            ),
696            record(
697                "B_SPLINE_SURFACE_WITH_KNOTS",
698                vec![u_multiplicities, v_multiplicities, u_knots, v_knots, form],
699            ),
700            record("GEOMETRIC_REPRESENTATION_ITEM", vec![]),
701            record(
702                "RATIONAL_B_SPLINE_SURFACE",
703                vec![Value::List(weight_rows.into_iter().map(reals).collect())],
704            ),
705            record("REPRESENTATION_ITEM", vec![string("")]),
706            record("SURFACE", vec![]),
707        ]))
708    }
709
710    fn edge(&mut self, id: EdgeId) -> GeopResult<u64> {
711        if let Some(&step) = self.edges.get(&id) {
712            return Ok(step);
713        }
714        let edge = self.model.get_edge(id)?;
715        let start = self.vertex(edge.start_vertex)?;
716        let end = self.vertex(edge.end_vertex)?;
717        let curve = self.curve(&edge.curve)?;
718        let step = self.writer.add(
719            "EDGE_CURVE",
720            vec![
721                string(&(self.name)(id.into())),
722                Value::Ref(start),
723                Value::Ref(end),
724                Value::Ref(curve),
725                logical(true),
726            ],
727        );
728        self.edges.insert(id, step);
729        Ok(step)
730    }
731
732    /// A face's boundary: a `FACE_OUTER_BOUND` for its `outer` one, a
733    /// `FACE_BOUND` for a hole, turned around if `reversed`.
734    fn bound(&mut self, boundary: BoundaryType, outer: bool, reversed: bool) -> GeopResult<u64> {
735        let lp = match boundary {
736            BoundaryType::Vertex(v) => {
737                let vertex = self.vertex(v)?;
738                self.writer
739                    .add("VERTEX_LOOP", vec![string(""), Value::Ref(vertex)])
740            }
741            BoundaryType::Loop(anchor) => {
742                let mut oriented = Vec::new();
743                let coedges: Vec<_> = self.model.iterate_loop_coedges(anchor).collect();
744                for coedge_id in coedges {
745                    let coedge = self.model.get_coedge(coedge_id)?;
746                    // A coedge sitting at a pole joins the edges either side
747                    // of it at the pole's vertex: STEP needs nothing for it.
748                    let CoedgeGeometry::Edge(edge) = coedge.geometry else {
749                        continue;
750                    };
751                    let forward = coedge.sense == Sense::Forward;
752                    let edge = self.edge(edge)?;
753                    oriented.push(self.writer.add(
754                        "ORIENTED_EDGE",
755                        vec![
756                            string(""),
757                            Value::Derived,
758                            Value::Derived,
759                            Value::Ref(edge),
760                            logical(forward),
761                        ],
762                    ));
763                }
764                self.writer
765                    .add("EDGE_LOOP", vec![string(""), refs(oriented)])
766            }
767        };
768        let kind = if outer {
769            "FACE_OUTER_BOUND"
770        } else {
771            "FACE_BOUND"
772        };
773        Ok(self
774            .writer
775            .add(kind, vec![string(""), Value::Ref(lp), logical(!reversed)]))
776    }
777
778    fn face(&mut self, id: FaceId, reversed: bool) -> GeopResult<u64> {
779        let face = self.model.get_face(id)?;
780        let surface = self.surface(&face.surface)?;
781        let mut bounds = vec![self.bound(face.outer, true, reversed)?];
782        for &hole in &face.holes {
783            bounds.push(self.bound(hole, false, reversed)?);
784        }
785        Ok(self.writer.add(
786            "ADVANCED_FACE",
787            vec![
788                string(&(self.name)(id.into())),
789                refs(bounds),
790                Value::Ref(surface),
791                logical(!reversed),
792            ],
793        ))
794    }
795
796    /// The shell `id` as a `kind` (`CLOSED_SHELL` or `OPEN_SHELL`), its
797    /// faces turned around if `reversed`.
798    fn shell(&mut self, id: ShellId, kind: &str, reversed: bool) -> GeopResult<u64> {
799        let mut faces = self.model.get_shell(id)?.faces.clone();
800        faces.sort_by_key(|id| id.0);
801        let mut steps = Vec::new();
802        for face in faces {
803            steps.push(self.face(face, reversed)?);
804        }
805        Ok(self.writer.add(kind, vec![string(""), refs(steps)]))
806    }
807}
808
809/// Writes the part as far as the program runs as a STEP file — the parts
810/// placed in it as an assembly (see [`write_part`]) — named after the
811/// program's file.
812pub struct Step;
813
814impl Exporter for Step {
815    const INFO: ExporterInfo = ExporterInfo {
816        kind: "step",
817        label: "STEP",
818        doc: "solids, for other CAD systems",
819        formats: &["step", "stp"],
820    };
821    type Options = NoOptions;
822
823    fn export(output: &mut Output, format: &str, _: NoOptions) -> GeopResult<Export> {
824        let name = stem(output.file_name());
825        let text = write_part(&output.part(), name)?;
826        Ok(Export {
827            name: format!("{name}.{format}"),
828            media_type: "model/step".into(),
829            content: Content::Text(text),
830        })
831    }
832}