Skip to main content

step_io/build/
mod.rs

1//! [`StepBuilder`] — the convenience authoring layer over the strict
2//! [`Ap242Author`] constructors.
3//!
4//! [`new`](StepBuilder::new) lays down the skeleton every exporter needs
5//! (application protocol, product contexts, units —
6//! [`new_with`](StepBuilder::new_with) to choose them);
7//! [`finish`](StepBuilder::finish) assembles everything added so far and
8//! returns the Part 21 text.
9//!
10//! The core is parts and their shape. [`part`](StepBuilder::part) adds one
11//! product with its full definition chain
12//! ([`part_with`](StepBuilder::part_with) also sets part number / version /
13//! descriptions). Topology mirrors the kernel's own —
14//! [`vertex`](StepBuilder::vertex) → [`edge`](StepBuilder::edge) →
15//! [`face`](StepBuilder::face) → [`solid`](StepBuilder::solid), with analytic
16//! surfaces and curves as plain inputs ([`SurfaceInput`], [`CurveInput`],
17//! [`ProfileInput`]). Assemblies are one
18//! [`place(parent, child, at)`](StepBuilder::place) call per placed
19//! instance, the child's definition shared between them.
20//!
21//! Everything else is one call each. [`mesh`](StepBuilder::mesh) attaches a
22//! display mesh (vertex array + index triangles), optionally linked to the
23//! b-rep solid it renders. [`style`](StepBuilder::style) colours a solid or
24//! a single face, with optional transparency; [`layer`](StepBuilder::layer),
25//! [`hide`](StepBuilder::hide), and [`hide_layer`](StepBuilder::hide_layer)
26//! manage presentation layers and default visibility.
27//! [`contributor`](StepBuilder::contributor),
28//! [`approve`](StepBuilder::approve), [`document`](StepBuilder::document),
29//! and [`security`](StepBuilder::security) attach PLM records to a part's
30//! version, with people as shared
31//! [`person_and_org`](StepBuilder::person_and_org) handles.
32//! [`header`](StepBuilder::header) fills the Part 21 file header.
33//!
34//! Anything the builder does not cover is still reachable:
35//! [`author`](StepBuilder::author) hands out the underlying [`Ap242Author`],
36//! and ids cross freely between the two levels.
37//!
38//! The minimal flow — a planar part handed over from a kernel:
39//!
40//! ```no_run
41//! use step_io::build::{CurveInput, FaceBoundInput, Frame, SurfaceInput};
42//!
43//! # struct KernelEdge { start: usize, end: usize }
44//! # struct KernelFace { origin: [f64; 3], normal: [f64; 3], x_dir: [f64; 3], bound: Vec<usize> }
45//! # struct Kernel { points: Vec<[f64; 3]>, edges: Vec<KernelEdge>, faces: Vec<KernelFace> }
46//! # let kernel = Kernel { points: vec![], edges: vec![], faces: vec![] };
47//! let mut b = step_io::StepBuilder::new().unwrap();
48//! let plate = b.part("plate").unwrap();
49//!
50//! let vs: Vec<_> = kernel.points.iter()
51//!     .map(|p| b.vertex(*p).unwrap())
52//!     .collect();
53//! let es: Vec<_> = kernel.edges.iter()
54//!     .map(|e| b.edge(vs[e.start], vs[e.end], CurveInput::Line).unwrap())
55//!     .collect();
56//! let fs: Vec<_> = kernel.faces.iter()
57//!     .map(|f| {
58//!         let plane = Frame { origin: f.origin, axis: f.normal, ref_dir: f.x_dir };
59//!         let bound = f.bound.iter().map(|&i| (es[i], true)).collect();
60//!         b.face(SurfaceInput::Plane(plane), true, vec![FaceBoundInput::outer(bound)])
61//!             .unwrap()
62//!     })
63//!     .collect();
64//! b.solid(plate, "plate body", fs).unwrap();
65//!
66//! std::fs::write("plate.step", b.finish().unwrap()).unwrap();
67//! ```
68//!
69//! The full tour — assembly, display mesh, presentation, metadata, header:
70//!
71//! ```no_run
72//! use step_io::build::{
73//!     ApprovalInput, Frame, HeaderInput, MeshInput, MeshNormalsInput, PersonOrg,
74//!     Rgb, StyleTarget,
75//! };
76//!
77//! # struct Kernel { mesh_points: Vec<[f64; 3]>, mesh_triangles: Vec<[usize; 3]> }
78//! # let kernel = Kernel { mesh_points: vec![], mesh_triangles: vec![] };
79//! # fn faces(_b: &mut step_io::StepBuilder) -> Vec<step_io::generated::model::AdvancedFaceId> { vec![] }
80//! let mut b = step_io::StepBuilder::new().unwrap();
81//! b.header(&HeaderInput {
82//!     file_name: Some("car.step".into()),
83//!     organizations: vec!["ACME".into()],
84//!     originating_system: Some("MyKernel 2.1".into()),
85//!     ..Default::default() // timestamp auto-stamped
86//! });
87//!
88//! let car = b.part("car").unwrap();
89//! let wheel = b.part("wheel").unwrap();
90//! let wheel_faces = faces(&mut b); // b-rep translated as in the minimal flow
91//! let body = b.solid(wheel, "wheel body", wheel_faces).unwrap();
92//!
93//! // One placed instance per call; the wheel's definition is shared.
94//! let z = [0.0, 0.0, 1.0];
95//! let x = [1.0, 0.0, 0.0];
96//! b.place(car, wheel, Frame { origin: [900.0, 700.0, 300.0], axis: z, ref_dir: x }).unwrap();
97//! b.place(car, wheel, Frame { origin: [900.0, -700.0, 300.0], axis: z, ref_dir: x }).unwrap();
98//!
99//! // Display mesh (the kernel's tessellation) linked to the b-rep it renders.
100//! b.mesh(wheel, "wheel mesh", &MeshInput {
101//!     points: kernel.mesh_points,
102//!     triangles: kernel.mesh_triangles,
103//!     normals: MeshNormalsInput::None,
104//! }, Some(body)).unwrap();
105//!
106//! // Presentation: colour/transparency, layers, default visibility.
107//! b.style(StyleTarget::Solid(body), Rgb { red: 0.8, green: 0.2, blue: 0.1 }, None).unwrap();
108//! let hidden = b.layer("REFERENCE", vec![StyleTarget::Solid(body)]).unwrap();
109//! b.hide_layer(hidden);
110//!
111//! // PLM metadata on the part's version.
112//! let john = b.person_and_org(&PersonOrg {
113//!     person_id: "p1".into(),
114//!     first_name: Some("John".into()),
115//!     last_name: Some("Doe".into()),
116//!     organization: "ACME".into(),
117//! }).unwrap();
118//! b.contributor(wheel, "creator", john).unwrap();
119//! b.approve(wheel, &ApprovalInput {
120//!     status: "approved".into(),
121//!     level: "final".into(),
122//!     approvers: vec![(john, "approver".into())],
123//!     date: None,
124//! }).unwrap();
125//!
126//! let text = b.finish().unwrap();
127//! # let _ = text;
128//! ```
129
130// Shared with the read side (`scene`): the NURBS forms `to_nurbs()` returns are
131// exactly what the builder accepts, and colours round-trip as the same type.
132use crate::emit::FileHeader;
133use crate::generated::author::{Ap242Author, AuthorError};
134use crate::generated::model as m;
135pub use crate::scene::{NurbsCurve, NurbsSurface, Rgb};
136
137/// Part 21 HEADER fields for [`StepBuilder::header`]. Everything defaults
138/// to empty except the pieces the builder supplies itself: the timestamp
139/// (current UTC time unless overridden) and the preprocessor stamp
140/// (always `step-io <version>`).
141#[derive(Debug, Clone, Default)]
142pub struct HeaderInput {
143    /// `FILE_NAME.name` — the customary file name.
144    pub file_name: Option<String>,
145    /// `FILE_DESCRIPTION` entry.
146    pub description: Option<String>,
147    pub authors: Vec<String>,
148    pub organizations: Vec<String>,
149    /// `FILE_NAME.originating_system` — the exporting kernel/application.
150    pub originating_system: Option<String>,
151    pub authorisation: Option<String>,
152    /// `None` = stamp the current UTC time; `Some` = used verbatim (fix it
153    /// for reproducible output, or `""` to leave the field blank).
154    pub timestamp: Option<String>,
155}
156
157/// Days-from-civil inverse (Howard Hinnant's algorithm): unix seconds to a
158/// `YYYY-MM-DDThh:mm:ssZ` UTC stamp.
159fn iso_utc_from_unix(secs: u64) -> String {
160    let days = secs / 86_400;
161    let rem = secs % 86_400;
162    let (hour, minute, second) = (rem / 3600, (rem % 3600) / 60, rem % 60);
163    let z = days + 719_468;
164    let era = z / 146_097;
165    let doe = z % 146_097;
166    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
167    let year = yoe + era * 400;
168    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
169    let mp = (5 * doy + 2) / 153;
170    let day = doy - (153 * mp + 2) / 5 + 1;
171    let month = if mp < 10 { mp + 3 } else { mp - 9 };
172    let year = if month <= 2 { year + 1 } else { year };
173    format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z")
174}
175
176/// The current time as an ISO 8601 UTC stamp.
177fn now_iso_utc() -> String {
178    let secs = std::time::SystemTime::now()
179        .duration_since(std::time::UNIX_EPOCH)
180        .map_or(0, |d| d.as_secs());
181    iso_utc_from_unix(secs)
182}
183
184/// The file's length unit, chosen at [`StepBuilder::new_with`] time (a
185/// unit context is a file-global entity every shape references).
186#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
187pub enum LengthUnit {
188    #[default]
189    Millimetre,
190    Metre,
191}
192
193/// Unit setup for [`StepBuilder::new_with`]; the default is what plain
194/// [`new`](StepBuilder::new) uses (millimetre, 1e-7 uncertainty).
195#[derive(Debug, Clone, Copy)]
196pub struct UnitsInput {
197    pub length: LengthUnit,
198    /// Length uncertainty (modelling precision), in the file's own length
199    /// unit — expected finite and positive (degenerate values are the
200    /// caller's responsibility).
201    pub uncertainty: f64,
202}
203
204impl Default for UnitsInput {
205    fn default() -> Self {
206        Self {
207            length: LengthUnit::Millimetre,
208            uncertainty: 1.0e-7,
209        }
210    }
211}
212
213/// A right-handed placement frame: `origin` plus the `axis` (local Z) and
214/// `ref_dir` (local X) directions — the builder's input for
215/// `AXIS2_PLACEMENT_3D`. Directions are passed through as given (kernels
216/// supply unit vectors); geometric well-formedness is the caller's
217/// responsibility.
218#[derive(Debug, Clone, Copy)]
219pub struct Frame {
220    pub origin: [f64; 3],
221    pub axis: [f64; 3],
222    pub ref_dir: [f64; 3],
223}
224
225/// Surface input for [`StepBuilder::face`]: the stored analytic forms, plus
226/// NURBS for everything else — [`NurbsSurface`] is the same type
227/// `Surface::to_nurbs()` returns on the read side, so read-side output can
228/// be fed straight back in. Non-rational data (all weights `1.0`) becomes a
229/// plain `B_SPLINE_SURFACE_WITH_KNOTS`; rational data becomes the customary
230/// multi-part complex instance.
231#[derive(Debug, Clone)]
232pub enum SurfaceInput {
233    Plane(Frame),
234    /// Cylinder around the frame's axis with the given radius.
235    Cylinder(Frame, f64),
236    /// Sphere centred at the frame's origin with the given radius.
237    Sphere(Frame, f64),
238    /// Torus around the frame's axis: major radius, then minor radius.
239    Torus(Frame, f64, f64),
240    /// Cone around the frame's axis: radius at the frame's origin, then the
241    /// half-angle in radians.
242    Cone(Frame, f64, f64),
243    /// Profile swept along an extrusion vector (direction × length).
244    LinearExtrusion(ProfileInput, [f64; 3]),
245    /// Profile revolved around an axis: axis origin, then axis direction.
246    Revolution(ProfileInput, [f64; 3], [f64; 3]),
247    Nurbs(NurbsSurface),
248}
249
250/// Curve input for [`StepBuilder::edge`]. [`NurbsCurve`] mirrors the
251/// read-side `to_nurbs()` type; see [`SurfaceInput`] for the
252/// rational/non-rational split.
253#[derive(Debug, Clone)]
254pub enum CurveInput {
255    /// Straight segment — computed from the two vertex positions.
256    Line,
257    /// Circle in the frame's XY plane with the given radius; with equal
258    /// end vertices this is a full circle, otherwise an arc.
259    Circle(Frame, f64),
260    /// Ellipse in the frame's XY plane: first and second semi-axis; with
261    /// equal end vertices a full ellipse, otherwise an arc.
262    Ellipse(Frame, f64, f64),
263    /// Piecewise-linear curve through the given points (customarily the
264    /// first/last coincide with the edge's vertices).
265    Polyline(Vec<[f64; 3]>),
266    Nurbs(NurbsCurve),
267}
268
269/// A profile curve for the swept surfaces — plain geometry, no vertices
270/// (unlike [`CurveInput::Line`], which derives from edge vertices).
271#[derive(Debug, Clone)]
272pub enum ProfileInput {
273    /// Infinite straight line: a point and a direction.
274    Line([f64; 3], [f64; 3]),
275    Circle(Frame, f64),
276    Ellipse(Frame, f64, f64),
277    Nurbs(NurbsCurve),
278}
279
280/// One face bound for [`StepBuilder::face`]: the loop's edges with their
281/// per-edge direction, whether this is the outer bound, and the bound's own
282/// orientation flag.
283#[derive(Debug, Clone)]
284pub struct FaceBoundInput {
285    pub edges: Vec<(m::EdgeCurveId, bool)>,
286    pub outer: bool,
287    pub orientation: bool,
288}
289
290impl FaceBoundInput {
291    /// The customary outer bound (orientation `true`).
292    #[must_use]
293    pub fn outer(edges: Vec<(m::EdgeCurveId, bool)>) -> Self {
294        Self {
295            edges,
296            outer: true,
297            orientation: true,
298        }
299    }
300
301    /// An inner (hole) bound (orientation `true`).
302    #[must_use]
303    pub fn inner(edges: Vec<(m::EdgeCurveId, bool)>) -> Self {
304        Self {
305            edges,
306            outer: false,
307            orientation: true,
308        }
309    }
310}
311
312/// Compress an expanded knot vector (each knot repeated by multiplicity —
313/// the [`NurbsCurve`]/[`NurbsSurface`] form) into STEP's distinct-knots +
314/// multiplicities pair. Exact equality groups repeats; the read side expands
315/// them back identically, so the round trip is lossless.
316fn compress_knots(expanded: &[f64]) -> (Vec<f64>, Vec<i64>) {
317    let mut knots: Vec<f64> = Vec::new();
318    let mut multiplicities: Vec<i64> = Vec::new();
319    for &k in expanded {
320        if knots.last() == Some(&k) {
321            *multiplicities.last_mut().unwrap() += 1;
322        } else {
323            knots.push(k);
324            multiplicities.push(1);
325        }
326    }
327    (knots, multiplicities)
328}
329
330/// Optional overrides for [`StepBuilder::part_with`] — every field defaults
331/// to the plain [`part`](StepBuilder::part) behaviour.
332#[derive(Debug, Clone, Default)]
333pub struct PartOptions {
334    /// `PRODUCT.id` (part number); defaults to the part name.
335    pub id: Option<String>,
336    /// `PRODUCT.description`.
337    pub description: Option<String>,
338    /// Version label (`PRODUCT_DEFINITION_FORMATION.id`); defaults to `""`.
339    pub version: Option<String>,
340    /// `PRODUCT_DEFINITION_FORMATION.description`.
341    pub version_description: Option<String>,
342    /// `PRODUCT_DEFINITION.description`.
343    pub definition_description: Option<String>,
344}
345
346/// A person at an organization, for [`StepBuilder::person_and_org`].
347#[derive(Debug, Clone)]
348pub struct PersonOrg {
349    pub person_id: String,
350    pub first_name: Option<String>,
351    pub last_name: Option<String>,
352    pub organization: String,
353}
354
355/// A calendar date with wall-clock time (no time zone — emitted as UTC).
356#[derive(Debug, Clone, Copy)]
357pub struct DateTimeInput {
358    pub year: i64,
359    pub month: i64,
360    pub day: i64,
361    pub hour: i64,
362    pub minute: Option<i64>,
363}
364
365/// One approval for [`StepBuilder::approve`]: a status word (customarily
366/// `"approved"`), a level, any number of approvers with their roles, and an
367/// optional approval date.
368#[derive(Debug, Clone, Default)]
369pub struct ApprovalInput {
370    pub status: String,
371    pub level: String,
372    pub approvers: Vec<(m::PersonAndOrganizationId, String)>,
373    pub date: Option<DateTimeInput>,
374}
375
376/// An external document reference for [`StepBuilder::document`].
377#[derive(Debug, Clone)]
378pub struct DocumentInput {
379    pub id: String,
380    pub name: String,
381    pub description: Option<String>,
382    /// `DOCUMENT_TYPE.product_data_type`, e.g. `"drawing"`.
383    pub kind: String,
384}
385
386/// Normals for a [`MeshInput`], mirroring what the read side exposes: none,
387/// one for the whole mesh, or one per vertex (row count must match the
388/// point count — a mismatch is the caller's responsibility).
389#[derive(Debug, Clone, Default)]
390pub enum MeshNormalsInput {
391    #[default]
392    None,
393    Uniform([f64; 3]),
394    PerVertex(Vec<[f64; 3]>),
395}
396
397/// A display mesh for [`StepBuilder::mesh`]: a vertex array plus 0-based
398/// index triangles — the plain form kernels and renderers already hold.
399#[derive(Debug, Clone)]
400pub struct MeshInput {
401    pub points: Vec<[f64; 3]>,
402    pub triangles: Vec<[usize; 3]>,
403    pub normals: MeshNormalsInput,
404}
405
406/// What a [`StepBuilder::style`] call colours: a whole solid or one face —
407/// the two targets the read-side `Scene` resolves styles for.
408#[derive(Debug, Clone, Copy)]
409pub enum StyleTarget {
410    Face(m::AdvancedFaceId),
411    Solid(m::ManifoldSolidBrepId),
412}
413
414/// Handle for a vertex created by [`StepBuilder::vertex`] — an index into
415/// the builder that created it (a different builder's handle panics or
416/// targets the wrong vertex). The builder keeps the vertex position so
417/// straight edges can derive their line geometry.
418#[derive(Debug, Copy, Clone, PartialEq, Eq)]
419pub struct Vertex(usize);
420
421/// Handle for a part created by [`StepBuilder::part`] — an index into the
422/// builder that created it. Passing it to a different builder panics (out
423/// of range) or silently targets the wrong part (in range).
424#[derive(Debug, Copy, Clone, PartialEq, Eq)]
425pub struct Part(usize);
426
427/// A part's product chain plus the shape items collected for it; the shape
428/// representation itself is materialized in [`StepBuilder::finish`] because
429/// representation items are fixed at insertion.
430struct PendingPart {
431    product: m::ProductId,
432    formation: m::ProductDefinitionFormationId,
433    definition: m::ProductDefinitionId,
434    shape: m::ProductDefinitionShapeId,
435    origin: m::Axis2Placement3dId,
436    items: Vec<m::RepresentationItemRef>,
437    meshes: Vec<m::TessellatedSolidId>,
438}
439
440/// One [`StepBuilder::place`] call: the eagerly-created usage occurrence
441/// plus the wiring deferred to [`StepBuilder::finish`] (the transformation
442/// relationship needs both parts' shape representations, which exist only
443/// then).
444struct PendingPlacement {
445    nauo: m::NextAssemblyUsageOccurrenceId,
446    parent: usize,
447    child: usize,
448    to: m::Axis2Placement3dId,
449}
450
451/// Ergonomic writer for the standard AP242 file shape: shared contexts and
452/// units up front, one product chain per [`part`](Self::part), everything
453/// bound and emitted by [`finish`](Self::finish).
454pub struct StepBuilder {
455    author: Ap242Author,
456    ctx: m::ComplexUnitId,
457    product_context: m::ProductContextId,
458    pd_context: m::ProductDefinitionContextId,
459    parts: Vec<PendingPart>,
460    placements: Vec<PendingPlacement>,
461    vertices: Vec<(m::VertexPointId, [f64; 3])>,
462    styles: Vec<m::StyledItemId>,
463    hidden: Vec<m::InvisibleItemRef>,
464    header: HeaderInput,
465}
466
467impl StepBuilder {
468    /// Set up the shared skeleton with the default units (millimetre,
469    /// 1e-7 uncertainty) — see [`new_with`](Self::new_with) to choose.
470    ///
471    /// # Errors
472    /// Propagates [`AuthorError`] from the strict constructors; the wiring
473    /// here is fixed, so an error indicates a bug in the builder itself.
474    pub fn new() -> Result<Self, AuthorError> {
475        Self::new_with(&UnitsInput::default())
476    }
477
478    /// Set up the shared skeleton: application context + protocol definition
479    /// (values are stamped with the AP242 profile on `finish`), product
480    /// contexts, and the geometric context complex carrying the chosen SI
481    /// length unit, radian, steradian, and the given length uncertainty.
482    ///
483    /// # Errors
484    /// Propagates [`AuthorError`] from the strict constructors; the wiring
485    /// here is fixed, so an error indicates a bug in the builder itself.
486    pub fn new_with(units: &UnitsInput) -> Result<Self, AuthorError> {
487        let mut a = Ap242Author::new();
488
489        let ac = a.add_application_context("managed model based 3d engineering".to_owned())?;
490        a.add_application_protocol_definition(
491            "international standard".to_owned(),
492            "ap242_managed_model_based_3d_engineering_mim_lf".to_owned(),
493            2011,
494            m::ApplicationContextRef::ApplicationContext(ac),
495        )?;
496        let product_context = a.add_product_context(
497            String::new(),
498            m::ApplicationContextRef::ApplicationContext(ac),
499            "mechanical".to_owned(),
500        )?;
501        let pd_context = a.add_product_definition_context(
502            "part definition".to_owned(),
503            m::ApplicationContextRef::ApplicationContext(ac),
504            "design".to_owned(),
505        )?;
506
507        let length_prefix = match units.length {
508            LengthUnit::Millimetre => Some(m::SiPrefix::Milli),
509            LengthUnit::Metre => None,
510        };
511        let mm = a.add_complex(vec![
512            m::UnitPart::LengthUnit,
513            m::UnitPart::NamedUnit { dimensions: None },
514            m::UnitPart::SiUnit {
515                prefix: length_prefix,
516                name: m::SiUnitName::Metre,
517            },
518        ])?;
519        let rad = a.add_complex(vec![
520            m::UnitPart::NamedUnit { dimensions: None },
521            m::UnitPart::PlaneAngleUnit,
522            m::UnitPart::SiUnit {
523                prefix: None,
524                name: m::SiUnitName::Radian,
525            },
526        ])?;
527        let sr = a.add_complex(vec![
528            m::UnitPart::NamedUnit { dimensions: None },
529            m::UnitPart::SiUnit {
530                prefix: None,
531                name: m::SiUnitName::Steradian,
532            },
533            m::UnitPart::SolidAngleUnit,
534        ])?;
535        let uncertainty = a.add_uncertainty_measure_with_unit(
536            m::MeasureValue {
537                type_name: Some("LENGTH_MEASURE".to_owned()),
538                value: m::MeasureScalar::Real(units.uncertainty),
539            },
540            m::UnitRef::Complex(mm),
541            "distance_accuracy_value".to_owned(),
542            Some("confusion accuracy".to_owned()),
543        )?;
544        let ctx = a.add_complex(vec![
545            m::UnitPart::GeometricRepresentationContext {
546                coordinate_space_dimension: 3,
547            },
548            m::UnitPart::GlobalUncertaintyAssignedContext {
549                uncertainty: vec![
550                    m::UncertaintyMeasureWithUnitRef::UncertaintyMeasureWithUnit(uncertainty),
551                ],
552            },
553            m::UnitPart::GlobalUnitAssignedContext {
554                units: vec![
555                    m::UnitRef::Complex(mm),
556                    m::UnitRef::Complex(rad),
557                    m::UnitRef::Complex(sr),
558                ],
559            },
560            m::UnitPart::RepresentationContext {
561                context_identifier: String::new(),
562                context_type: "3D".to_owned(),
563            },
564        ])?;
565
566        Ok(Self {
567            author: a,
568            ctx,
569            product_context,
570            pd_context,
571            parts: Vec::new(),
572            placements: Vec::new(),
573            vertices: Vec::new(),
574            styles: Vec::new(),
575            hidden: Vec::new(),
576            header: HeaderInput::default(),
577        })
578    }
579
580    /// Set the Part 21 HEADER fields (file name, description, authors,
581    /// organizations, originating system, authorisation, timestamp) applied
582    /// on [`finish`](Self::finish). May be called any time before `finish`;
583    /// the last call wins. Without it the header carries only the automatic
584    /// timestamp and preprocessor stamp.
585    pub fn header(&mut self, h: &HeaderInput) {
586        self.header = h.clone();
587    }
588
589    /// Escape hatch to the strict low-level layer — the write-side
590    /// counterpart of the read side's `Scene::model()`. Entities the builder
591    /// does not cover go through [`Ap242Author`]'s generated constructors
592    /// directly; ids cross freely in both directions (builder-produced ids
593    /// feed low-level calls and vice versa — same model, same validation).
594    ///
595    /// Only borrowed access is offered: taking the author out would bypass
596    /// [`finish`](Self::finish), which still has to materialize the pending
597    /// shape representations, placements, style anchors, category, and
598    /// header.
599    pub fn author(&mut self) -> &mut Ap242Author {
600        &mut self.author
601    }
602
603    /// Colour `target` (a solid or one face), optionally with a transparency
604    /// (`0.0` = opaque, `1.0` = fully transparent). Styling the same target
605    /// again adds another style record; readers use the first they find.
606    ///
607    /// # Errors
608    /// An id that did not come from this builder (another builder's, or out
609    /// of range) fails validation with [`AuthorError`]; beyond that the
610    /// wiring is fixed, so an error indicates a bug in the builder itself.
611    pub fn style(
612        &mut self,
613        target: StyleTarget,
614        color: Rgb,
615        transparency: Option<f64>,
616    ) -> Result<m::StyledItemId, AuthorError> {
617        let a = &mut self.author;
618        let rgb = a.add_colour_rgb(String::new(), color.red, color.green, color.blue)?;
619        // Colour alone rides the customary fill-area chain; with a
620        // transparency the rendering form carries both.
621        let element = if let Some(t) = transparency {
622            let transparent = a.add_surface_style_transparent(t)?;
623            let rendering = a.add_surface_style_rendering_with_properties(
624                m::ShadingSurfaceMethod::NormalShading,
625                m::ColourRef::ColourRgb(rgb),
626                vec![m::RenderingPropertiesSelectRef::SurfaceStyleTransparent(
627                    transparent,
628                )],
629            )?;
630            m::SurfaceStyleElementSelectRef::SurfaceStyleRenderingWithProperties(rendering)
631        } else {
632            let fill_colour =
633                a.add_fill_area_style_colour(String::new(), m::ColourRef::ColourRgb(rgb))?;
634            let fill_style = a.add_fill_area_style(
635                String::new(),
636                vec![m::FillStyleSelectRef::FillAreaStyleColour(fill_colour)],
637            )?;
638            let fill_area =
639                a.add_surface_style_fill_area(m::FillAreaStyleRef::FillAreaStyle(fill_style))?;
640            m::SurfaceStyleElementSelectRef::SurfaceStyleFillArea(fill_area)
641        };
642        let side_style = a.add_surface_side_style(String::new(), vec![element])?;
643        let usage = a.add_surface_style_usage(
644            m::SurfaceSide::Both,
645            m::SurfaceSideStyleSelectRef::SurfaceSideStyle(side_style),
646        )?;
647        let assignment = a.add_presentation_style_assignment(vec![
648            m::PresentationStyleSelectRef::SurfaceStyleUsage(usage),
649        ])?;
650        let item = match target {
651            StyleTarget::Face(f) => m::StyledItemTargetRef::AdvancedFace(f),
652            StyleTarget::Solid(s) => m::StyledItemTargetRef::ManifoldSolidBrep(s),
653        };
654        let styled = a.add_styled_item(
655            String::new(),
656            vec![m::PresentationStyleAssignmentRef::PresentationStyleAssignment(assignment)],
657            item,
658        )?;
659        self.styles.push(styled);
660        Ok(styled)
661    }
662
663    /// Assign `targets` to a named presentation layer; the returned id can
664    /// be fed to [`hide_layer`](Self::hide_layer) to switch the whole layer
665    /// off by default.
666    ///
667    /// # Errors
668    /// An id that did not come from this builder (another builder's, or out
669    /// of range) fails validation with [`AuthorError`]; beyond that the
670    /// wiring is fixed, so an error indicates a bug in the builder itself.
671    pub fn layer(
672        &mut self,
673        name: &str,
674        targets: Vec<StyleTarget>,
675    ) -> Result<m::PresentationLayerAssignmentId, AuthorError> {
676        let items = targets
677            .into_iter()
678            .map(|t| match t {
679                StyleTarget::Face(f) => m::LayeredItemRef::AdvancedFace(f),
680                StyleTarget::Solid(s) => m::LayeredItemRef::ManifoldSolidBrep(s),
681            })
682            .collect();
683        self.author
684            .add_presentation_layer_assignment(name.to_owned(), String::new(), items)
685    }
686
687    /// Mark `target` invisible by default. Implemented the way STEP requires
688    /// it: an `INVISIBILITY` cannot point at geometry directly, so the
689    /// builder attaches an empty (`.NULL.`-styled) styled item to the target
690    /// and registers that — colour styles on the same target are unaffected.
691    ///
692    /// # Errors
693    /// An id that did not come from this builder (another builder's, or out
694    /// of range) fails validation with [`AuthorError`]; beyond that the
695    /// wiring is fixed, so an error indicates a bug in the builder itself.
696    pub fn hide(&mut self, target: StyleTarget) -> Result<(), AuthorError> {
697        let a = &mut self.author;
698        let assignment =
699            a.add_presentation_style_assignment(vec![m::PresentationStyleSelectRef::NullStyle(
700                m::NullStyle::Null,
701            )])?;
702        let item = match target {
703            StyleTarget::Face(f) => m::StyledItemTargetRef::AdvancedFace(f),
704            StyleTarget::Solid(s) => m::StyledItemTargetRef::ManifoldSolidBrep(s),
705        };
706        let styled = a.add_styled_item(
707            String::new(),
708            vec![m::PresentationStyleAssignmentRef::PresentationStyleAssignment(assignment)],
709            item,
710        )?;
711        self.styles.push(styled);
712        self.hidden.push(m::InvisibleItemRef::StyledItem(styled));
713        Ok(())
714    }
715
716    /// Switch a whole layer (from [`layer`](Self::layer)) invisible by
717    /// default. Infallible at call time — a foreign or stale id is caught by
718    /// the `INVISIBILITY` validation inside [`finish`](Self::finish).
719    pub fn hide_layer(&mut self, layer: m::PresentationLayerAssignmentId) {
720        self.hidden
721            .push(m::InvisibleItemRef::PresentationLayerAssignment(layer));
722    }
723
724    /// `AXIS2_PLACEMENT_3D` from a [`Frame`].
725    fn placement(&mut self, f: &Frame) -> Result<m::Axis2Placement3dId, AuthorError> {
726        let a = &mut self.author;
727        let location = a.add_cartesian_point(String::new(), f.origin.to_vec())?;
728        let axis = a.add_direction(String::new(), f.axis.to_vec())?;
729        let ref_direction = a.add_direction(String::new(), f.ref_dir.to_vec())?;
730        a.add_axis2_placement3d(
731            String::new(),
732            m::CartesianPointRef::CartesianPoint(location),
733            Some(m::DirectionRef::Direction(axis)),
734            Some(m::DirectionRef::Direction(ref_direction)),
735        )
736    }
737
738    /// One `CARTESIAN_POINT` ref per control point.
739    fn control_points(
740        &mut self,
741        row: &[[f64; 3]],
742    ) -> Result<Vec<m::CartesianPointRef>, AuthorError> {
743        row.iter()
744            .map(|p| {
745                self.author
746                    .add_cartesian_point(String::new(), p.to_vec())
747                    .map(m::CartesianPointRef::CartesianPoint)
748            })
749            .collect()
750    }
751
752    /// Geometry for a swept-surface profile (also backs the ellipse edge).
753    fn profile_geometry(&mut self, profile: &ProfileInput) -> Result<m::CurveRef, AuthorError> {
754        match profile {
755            ProfileInput::Line(point, dir) => {
756                let a = &mut self.author;
757                let pnt = a.add_cartesian_point(String::new(), point.to_vec())?;
758                let orientation = a.add_direction(String::new(), dir.to_vec())?;
759                let vector =
760                    a.add_vector(String::new(), m::DirectionRef::Direction(orientation), 1.0)?;
761                let line = a.add_line(
762                    String::new(),
763                    m::CartesianPointRef::CartesianPoint(pnt),
764                    m::VectorRef::Vector(vector),
765                )?;
766                Ok(m::CurveRef::Line(line))
767            }
768            ProfileInput::Circle(frame, radius) => {
769                let position = self.placement(frame)?;
770                let circle = self.author.add_circle(
771                    String::new(),
772                    m::Axis2PlacementRef::Axis2Placement3d(position),
773                    *radius,
774                )?;
775                Ok(m::CurveRef::Circle(circle))
776            }
777            ProfileInput::Ellipse(frame, semi_1, semi_2) => {
778                let position = self.placement(frame)?;
779                let ellipse = self.author.add_ellipse(
780                    String::new(),
781                    m::Axis2PlacementRef::Axis2Placement3d(position),
782                    *semi_1,
783                    *semi_2,
784                )?;
785                Ok(m::CurveRef::Ellipse(ellipse))
786            }
787            ProfileInput::Nurbs(curve) => self.nurbs_curve_geometry(curve),
788        }
789    }
790
791    /// NURBS curve geometry: non-rational (all weights `1.0`) as a plain
792    /// `B_SPLINE_CURVE_WITH_KNOTS`, rational as the customary multi-part
793    /// complex instance.
794    // Exact comparison intended: only weights that are exactly 1.0 take the
795    // non-rational form; anything else keeps its value in weights_data.
796    #[allow(clippy::float_cmp)]
797    fn nurbs_curve_geometry(&mut self, c: &NurbsCurve) -> Result<m::CurveRef, AuthorError> {
798        let cps = self.control_points(&c.control_points)?;
799        let (knots, knot_multiplicities) = compress_knots(&c.knots);
800        let degree = i64::try_from(c.degree).unwrap_or(i64::MAX);
801        if c.weights.iter().all(|w| *w == 1.0) {
802            let id = self.author.add_b_spline_curve_with_knots(
803                String::new(),
804                degree,
805                cps,
806                m::BSplineCurveForm::Unspecified,
807                m::Logical::Unknown,
808                m::Logical::Unknown,
809                knot_multiplicities,
810                knots,
811                m::KnotType::Unspecified,
812            )?;
813            Ok(m::CurveRef::BSplineCurveWithKnots(id))
814        } else {
815            let id = self.author.add_complex(vec![
816                m::UnitPart::BoundedCurve,
817                m::UnitPart::BSplineCurve {
818                    degree,
819                    control_points_list: cps,
820                    curve_form: m::BSplineCurveForm::Unspecified,
821                    closed_curve: m::Logical::Unknown,
822                    self_intersect: m::Logical::Unknown,
823                },
824                m::UnitPart::BSplineCurveWithKnots {
825                    knot_multiplicities,
826                    knots,
827                    knot_spec: m::KnotType::Unspecified,
828                },
829                m::UnitPart::Curve,
830                m::UnitPart::GeometricRepresentationItem,
831                m::UnitPart::RationalBSplineCurve {
832                    weights_data: c.weights.clone(),
833                },
834                m::UnitPart::RepresentationItem {
835                    name: String::new(),
836                },
837            ])?;
838            Ok(m::CurveRef::Complex(id))
839        }
840    }
841
842    /// NURBS surface geometry; see [`Self::nurbs_curve_geometry`].
843    #[allow(clippy::float_cmp)] // exact 1.0 check, as in nurbs_curve_geometry
844    fn nurbs_surface_geometry(&mut self, s: &NurbsSurface) -> Result<m::SurfaceRef, AuthorError> {
845        let mut cps = Vec::with_capacity(s.control_points.len());
846        for row in &s.control_points {
847            cps.push(self.control_points(row)?);
848        }
849        let (u_knots, u_multiplicities) = compress_knots(&s.knots_u);
850        let (v_knots, v_multiplicities) = compress_knots(&s.knots_v);
851        let u_degree = i64::try_from(s.degree_u).unwrap_or(i64::MAX);
852        let v_degree = i64::try_from(s.degree_v).unwrap_or(i64::MAX);
853        if s.weights.iter().flatten().all(|w| *w == 1.0) {
854            let id = self.author.add_b_spline_surface_with_knots(
855                String::new(),
856                u_degree,
857                v_degree,
858                cps,
859                m::BSplineSurfaceForm::Unspecified,
860                m::Logical::Unknown,
861                m::Logical::Unknown,
862                m::Logical::Unknown,
863                u_multiplicities,
864                v_multiplicities,
865                u_knots,
866                v_knots,
867                m::KnotType::Unspecified,
868            )?;
869            Ok(m::SurfaceRef::BSplineSurfaceWithKnots(id))
870        } else {
871            let id = self.author.add_complex(vec![
872                m::UnitPart::BoundedSurface,
873                m::UnitPart::BSplineSurface {
874                    u_degree,
875                    v_degree,
876                    control_points_list: cps,
877                    surface_form: m::BSplineSurfaceForm::Unspecified,
878                    u_closed: m::Logical::Unknown,
879                    v_closed: m::Logical::Unknown,
880                    self_intersect: m::Logical::Unknown,
881                },
882                m::UnitPart::BSplineSurfaceWithKnots {
883                    u_multiplicities,
884                    v_multiplicities,
885                    u_knots,
886                    v_knots,
887                    knot_spec: m::KnotType::Unspecified,
888                },
889                m::UnitPart::GeometricRepresentationItem,
890                m::UnitPart::RationalBSplineSurface {
891                    weights_data: s.weights.clone(),
892                },
893                m::UnitPart::RepresentationItem {
894                    name: String::new(),
895                },
896                m::UnitPart::Surface,
897            ])?;
898            Ok(m::SurfaceRef::Complex(id))
899        }
900    }
901
902    /// Add a topological vertex at `p`.
903    ///
904    /// # Errors
905    /// Propagates [`AuthorError`] from the strict constructors; the wiring
906    /// here is fixed, so an error indicates a bug in the builder itself.
907    pub fn vertex(&mut self, p: [f64; 3]) -> Result<Vertex, AuthorError> {
908        let point = self.author.add_cartesian_point(String::new(), p.to_vec())?;
909        let vp = self
910            .author
911            .add_vertex_point(String::new(), m::PointRef::CartesianPoint(point))?;
912        self.vertices.push((vp, p));
913        Ok(Vertex(self.vertices.len() - 1))
914    }
915
916    /// Add an edge between two vertices over the given curve. A straight
917    /// edge derives its line from the vertex positions (a zero-length
918    /// segment gets a placeholder direction — degenerate geometry is the
919    /// caller's responsibility); a circle with `from == to` is a closed
920    /// full-circle edge.
921    ///
922    /// # Errors
923    /// Propagates [`AuthorError`] from the strict constructors; the wiring
924    /// here is fixed, so an error indicates a bug in the builder itself.
925    pub fn edge(
926        &mut self,
927        from: Vertex,
928        to: Vertex,
929        curve: CurveInput,
930    ) -> Result<m::EdgeCurveId, AuthorError> {
931        let (start_vertex, start) = self.vertices[from.0];
932        let (end_vertex, end) = self.vertices[to.0];
933        let geometry = match curve {
934            CurveInput::Line => {
935                let delta = [end[0] - start[0], end[1] - start[1], end[2] - start[2]];
936                let len = (delta[0] * delta[0] + delta[1] * delta[1] + delta[2] * delta[2]).sqrt();
937                let dir = if len < 1e-12 {
938                    [0.0, 0.0, 1.0]
939                } else {
940                    [delta[0] / len, delta[1] / len, delta[2] / len]
941                };
942                let a = &mut self.author;
943                let pnt = a.add_cartesian_point(String::new(), start.to_vec())?;
944                let orientation = a.add_direction(String::new(), dir.to_vec())?;
945                let vector =
946                    a.add_vector(String::new(), m::DirectionRef::Direction(orientation), len)?;
947                let line = a.add_line(
948                    String::new(),
949                    m::CartesianPointRef::CartesianPoint(pnt),
950                    m::VectorRef::Vector(vector),
951                )?;
952                m::CurveRef::Line(line)
953            }
954            CurveInput::Circle(frame, radius) => {
955                let position = self.placement(&frame)?;
956                let circle = self.author.add_circle(
957                    String::new(),
958                    m::Axis2PlacementRef::Axis2Placement3d(position),
959                    radius,
960                )?;
961                m::CurveRef::Circle(circle)
962            }
963            CurveInput::Ellipse(frame, semi_1, semi_2) => {
964                self.profile_geometry(&ProfileInput::Ellipse(frame, semi_1, semi_2))?
965            }
966            CurveInput::Polyline(points) => {
967                let refs = self.control_points(&points)?;
968                m::CurveRef::Polyline(self.author.add_polyline(String::new(), refs)?)
969            }
970            CurveInput::Nurbs(curve) => self.nurbs_curve_geometry(&curve)?,
971        };
972        self.author.add_edge_curve(
973            String::new(),
974            m::VertexRef::VertexPoint(start_vertex),
975            m::VertexRef::VertexPoint(end_vertex),
976            geometry,
977            true,
978        )
979    }
980
981    /// Emit the geometry entity for a [`SurfaceInput`].
982    fn surface_geometry(&mut self, surface: SurfaceInput) -> Result<m::SurfaceRef, AuthorError> {
983        Ok(match surface {
984            SurfaceInput::Plane(frame) => {
985                let position = self.placement(&frame)?;
986                let plane = self.author.add_plane(
987                    String::new(),
988                    m::Axis2Placement3dRef::Axis2Placement3d(position),
989                )?;
990                m::SurfaceRef::Plane(plane)
991            }
992            SurfaceInput::Cylinder(frame, radius) => {
993                let position = self.placement(&frame)?;
994                let cyl = self.author.add_cylindrical_surface(
995                    String::new(),
996                    m::Axis2Placement3dRef::Axis2Placement3d(position),
997                    radius,
998                )?;
999                m::SurfaceRef::CylindricalSurface(cyl)
1000            }
1001            SurfaceInput::Sphere(frame, radius) => {
1002                let position = self.placement(&frame)?;
1003                let sphere = self.author.add_spherical_surface(
1004                    String::new(),
1005                    m::Axis2Placement3dRef::Axis2Placement3d(position),
1006                    radius,
1007                )?;
1008                m::SurfaceRef::SphericalSurface(sphere)
1009            }
1010            SurfaceInput::Torus(frame, major_radius, minor_radius) => {
1011                let position = self.placement(&frame)?;
1012                let torus = self.author.add_toroidal_surface(
1013                    String::new(),
1014                    m::Axis2Placement3dRef::Axis2Placement3d(position),
1015                    major_radius,
1016                    minor_radius,
1017                )?;
1018                m::SurfaceRef::ToroidalSurface(torus)
1019            }
1020            SurfaceInput::Cone(frame, radius, semi_angle) => {
1021                let position = self.placement(&frame)?;
1022                let cone = self.author.add_conical_surface(
1023                    String::new(),
1024                    m::Axis2Placement3dRef::Axis2Placement3d(position),
1025                    radius,
1026                    semi_angle,
1027                )?;
1028                m::SurfaceRef::ConicalSurface(cone)
1029            }
1030            SurfaceInput::LinearExtrusion(profile, sweep) => {
1031                let swept_curve = self.profile_geometry(&profile)?;
1032                let len = (sweep[0] * sweep[0] + sweep[1] * sweep[1] + sweep[2] * sweep[2]).sqrt();
1033                let dir = if len < 1e-12 {
1034                    [0.0, 0.0, 1.0]
1035                } else {
1036                    [sweep[0] / len, sweep[1] / len, sweep[2] / len]
1037                };
1038                let a = &mut self.author;
1039                let orientation = a.add_direction(String::new(), dir.to_vec())?;
1040                let vector =
1041                    a.add_vector(String::new(), m::DirectionRef::Direction(orientation), len)?;
1042                let surf = a.add_surface_of_linear_extrusion(
1043                    String::new(),
1044                    swept_curve,
1045                    m::VectorRef::Vector(vector),
1046                )?;
1047                m::SurfaceRef::SurfaceOfLinearExtrusion(surf)
1048            }
1049            SurfaceInput::Revolution(profile, origin, axis) => {
1050                let swept_curve = self.profile_geometry(&profile)?;
1051                let a = &mut self.author;
1052                let location = a.add_cartesian_point(String::new(), origin.to_vec())?;
1053                let axis_dir = a.add_direction(String::new(), axis.to_vec())?;
1054                let position = a.add_axis1_placement(
1055                    String::new(),
1056                    m::CartesianPointRef::CartesianPoint(location),
1057                    Some(m::DirectionRef::Direction(axis_dir)),
1058                )?;
1059                let surf = a.add_surface_of_revolution(
1060                    String::new(),
1061                    swept_curve,
1062                    m::Axis1PlacementRef::Axis1Placement(position),
1063                )?;
1064                m::SurfaceRef::SurfaceOfRevolution(surf)
1065            }
1066            SurfaceInput::Nurbs(surface) => self.nurbs_surface_geometry(&surface)?,
1067        })
1068    }
1069
1070    /// Add a face over an analytic surface, bounded by the given loops.
1071    ///
1072    /// # Errors
1073    /// An id that did not come from this builder (another builder's, or out
1074    /// of range) fails validation with [`AuthorError`]; beyond that the
1075    /// wiring is fixed, so an error indicates a bug in the builder itself.
1076    pub fn face(
1077        &mut self,
1078        surface: SurfaceInput,
1079        same_sense: bool,
1080        bounds: Vec<FaceBoundInput>,
1081    ) -> Result<m::AdvancedFaceId, AuthorError> {
1082        let face_geometry = self.surface_geometry(surface)?;
1083        let mut face_bounds = Vec::with_capacity(bounds.len());
1084        for bound in bounds {
1085            let mut edge_list = Vec::with_capacity(bound.edges.len());
1086            for (edge, forward) in bound.edges {
1087                let oe = self.author.add_oriented_edge(
1088                    String::new(),
1089                    m::EdgeRef::EdgeCurve(edge),
1090                    forward,
1091                )?;
1092                edge_list.push(m::OrientedEdgeRef::OrientedEdge(oe));
1093            }
1094            let el = self.author.add_edge_loop(String::new(), edge_list)?;
1095            let fb = if bound.outer {
1096                m::FaceBoundRef::FaceOuterBound(self.author.add_face_outer_bound(
1097                    String::new(),
1098                    m::LoopRef::EdgeLoop(el),
1099                    bound.orientation,
1100                )?)
1101            } else {
1102                m::FaceBoundRef::FaceBound(self.author.add_face_bound(
1103                    String::new(),
1104                    m::LoopRef::EdgeLoop(el),
1105                    bound.orientation,
1106                )?)
1107            };
1108            face_bounds.push(fb);
1109        }
1110        self.author
1111            .add_advanced_face(String::new(), face_bounds, face_geometry, same_sense)
1112    }
1113
1114    /// Close the faces into a shell and add the solid to `part`; the solid
1115    /// lands in the part's shape representation on [`finish`](Self::finish)
1116    /// (as an `ADVANCED_BREP_SHAPE_REPRESENTATION`).
1117    ///
1118    /// # Errors
1119    /// An id that did not come from this builder (another builder's, or out
1120    /// of range) fails validation with [`AuthorError`]; beyond that the
1121    /// wiring is fixed, so an error indicates a bug in the builder itself.
1122    pub fn solid(
1123        &mut self,
1124        part: Part,
1125        name: &str,
1126        faces: Vec<m::AdvancedFaceId>,
1127    ) -> Result<m::ManifoldSolidBrepId, AuthorError> {
1128        let shell = self.author.add_closed_shell(
1129            String::new(),
1130            faces.into_iter().map(m::FaceRef::AdvancedFace).collect(),
1131        )?;
1132        let solid = self
1133            .author
1134            .add_manifold_solid_brep(name.to_owned(), m::ClosedShellRef::ClosedShell(shell))?;
1135        self.parts[part.0]
1136            .items
1137            .push(m::RepresentationItemRef::ManifoldSolidBrep(solid));
1138        Ok(solid)
1139    }
1140
1141    /// Add one part: the full product chain (product → formation →
1142    /// definition → definition shape) plus its origin placement. Shape items
1143    /// collected for the part are bound into its shape representation by
1144    /// [`finish`](Self::finish).
1145    ///
1146    /// # Errors
1147    /// Propagates [`AuthorError`] from the strict constructors; the wiring
1148    /// here is fixed, so an error indicates a bug in the builder itself.
1149    pub fn part(&mut self, name: &str) -> Result<Part, AuthorError> {
1150        self.part_with(name, &PartOptions::default())
1151    }
1152
1153    /// [`part`](Self::part) with metadata overrides: part number, version
1154    /// label, and the description fields.
1155    ///
1156    /// # Errors
1157    /// Propagates [`AuthorError`] from the strict constructors; the wiring
1158    /// here is fixed, so an error indicates a bug in the builder itself.
1159    pub fn part_with(&mut self, name: &str, opts: &PartOptions) -> Result<Part, AuthorError> {
1160        let origin = self.placement(&Frame {
1161            origin: [0.0; 3],
1162            axis: [0.0, 0.0, 1.0],
1163            ref_dir: [1.0, 0.0, 0.0],
1164        })?;
1165        let a = &mut self.author;
1166        let product = a.add_product(
1167            opts.id.clone().unwrap_or_else(|| name.to_owned()),
1168            name.to_owned(),
1169            opts.description.clone(),
1170            vec![m::ProductContextRef::ProductContext(self.product_context)],
1171        )?;
1172        let formation = a.add_product_definition_formation(
1173            opts.version.clone().unwrap_or_default(),
1174            opts.version_description.clone(),
1175            m::ProductRef::Product(product),
1176        )?;
1177        let definition = a.add_product_definition(
1178            "design".to_owned(),
1179            opts.definition_description.clone(),
1180            m::ProductDefinitionFormationRef::ProductDefinitionFormation(formation),
1181            m::ProductDefinitionContextRef::ProductDefinitionContext(self.pd_context),
1182        )?;
1183        let shape = a.add_product_definition_shape(
1184            String::new(),
1185            None,
1186            m::CharacterizedDefinitionRef::ProductDefinition(definition),
1187        )?;
1188
1189        self.parts.push(PendingPart {
1190            product,
1191            formation,
1192            definition,
1193            shape,
1194            origin,
1195            items: Vec::new(),
1196            meshes: Vec::new(),
1197        });
1198        Ok(Part(self.parts.len() - 1))
1199    }
1200
1201    /// Attach a display mesh to `part`, optionally linked to the b-rep solid
1202    /// it tessellates (the link is what `MeshGroup::solid()` resolves on the
1203    /// read side). Triangle indices are 0-based into `points`; the builder
1204    /// converts to STEP's 1-based strip encoding. The mesh lands in its own
1205    /// `TESSELLATED_SHAPE_REPRESENTATION` on [`finish`](Self::finish).
1206    ///
1207    /// # Errors
1208    /// An id that did not come from this builder (another builder's, or out
1209    /// of range) fails validation with [`AuthorError`]; beyond that the
1210    /// wiring is fixed, so an error indicates a bug in the builder itself.
1211    pub fn mesh(
1212        &mut self,
1213        part: Part,
1214        name: &str,
1215        input: &MeshInput,
1216        of_solid: Option<m::ManifoldSolidBrepId>,
1217    ) -> Result<m::TessellatedSolidId, AuthorError> {
1218        let a = &mut self.author;
1219        let npoints = i64::try_from(input.points.len()).unwrap_or(i64::MAX);
1220        let coordinates = a.add_coordinates_list(
1221            String::new(),
1222            npoints,
1223            input.points.iter().map(|p| p.to_vec()).collect(),
1224        )?;
1225        let normals: Vec<Vec<f64>> = match &input.normals {
1226            MeshNormalsInput::None => Vec::new(),
1227            MeshNormalsInput::Uniform(n) => vec![n.to_vec()],
1228            MeshNormalsInput::PerVertex(rows) => rows.iter().map(|n| n.to_vec()).collect(),
1229        };
1230        // One 3-index strip per triangle, 1-based. The read side's strip
1231        // decode swaps the last two indices of the first window, so encode
1232        // (p, q, r) as [p, r, q] to get the original winding back.
1233        let to_ix = |v: usize| i64::try_from(v + 1).unwrap_or(i64::MAX);
1234        let triangle_strips: Vec<Vec<i64>> = input
1235            .triangles
1236            .iter()
1237            .map(|&[p, q, r]| vec![to_ix(p), to_ix(r), to_ix(q)])
1238            .collect();
1239        let face = a.add_complex_triangulated_face(
1240            name.to_owned(),
1241            m::CoordinatesListRef::CoordinatesList(coordinates),
1242            npoints,
1243            normals,
1244            None,
1245            Vec::new(),
1246            triangle_strips,
1247            Vec::new(),
1248        )?;
1249        let solid = a.add_tessellated_solid(
1250            name.to_owned(),
1251            vec![m::TessellatedStructuredItemRef::ComplexTriangulatedFace(
1252                face,
1253            )],
1254            of_solid.map(m::ManifoldSolidBrepRef::ManifoldSolidBrep),
1255        )?;
1256        self.parts[part.0].meshes.push(solid);
1257        Ok(solid)
1258    }
1259
1260    /// A person at an organization — reusable across
1261    /// [`contributor`](Self::contributor) and approver roles.
1262    ///
1263    /// # Errors
1264    /// Propagates [`AuthorError`] from the strict constructors; the wiring
1265    /// here is fixed, so an error indicates a bug in the builder itself.
1266    pub fn person_and_org(
1267        &mut self,
1268        p: &PersonOrg,
1269    ) -> Result<m::PersonAndOrganizationId, AuthorError> {
1270        let a = &mut self.author;
1271        let person = a.add_person(
1272            p.person_id.clone(),
1273            p.last_name.clone(),
1274            p.first_name.clone(),
1275            None,
1276            None,
1277            None,
1278        )?;
1279        let organization = a.add_organization(None, p.organization.clone(), None)?;
1280        a.add_person_and_organization(
1281            m::PersonRef::Person(person),
1282            m::OrganizationRef::Organization(organization),
1283        )
1284    }
1285
1286    /// Record `who` as a contributor to the part's version under the given
1287    /// role (customary roles: `"creator"`, `"design_owner"`, `"checker"`).
1288    ///
1289    /// # Errors
1290    /// An id that did not come from this builder (another builder's, or out
1291    /// of range) fails validation with [`AuthorError`]; beyond that the
1292    /// wiring is fixed, so an error indicates a bug in the builder itself.
1293    pub fn contributor(
1294        &mut self,
1295        part: Part,
1296        role: &str,
1297        who: m::PersonAndOrganizationId,
1298    ) -> Result<(), AuthorError> {
1299        let a = &mut self.author;
1300        let role = a.add_person_and_organization_role(role.to_owned())?;
1301        a.add_applied_person_and_organization_assignment(
1302            m::PersonAndOrganizationRef::PersonAndOrganization(who),
1303            m::PersonAndOrganizationRoleRef::PersonAndOrganizationRole(role),
1304            vec![m::PersonAndOrganizationItemRef::ProductDefinitionFormation(
1305                self.parts[part.0].formation,
1306            )],
1307        )?;
1308        Ok(())
1309    }
1310
1311    /// Attach an approval to the part's version: a status word, a level,
1312    /// any approvers with their roles, and an optional approval date.
1313    ///
1314    /// # Errors
1315    /// An id that did not come from this builder (another builder's, or out
1316    /// of range) fails validation with [`AuthorError`]; beyond that the
1317    /// wiring is fixed, so an error indicates a bug in the builder itself.
1318    pub fn approve(
1319        &mut self,
1320        part: Part,
1321        input: &ApprovalInput,
1322    ) -> Result<m::ApprovalId, AuthorError> {
1323        let a = &mut self.author;
1324        let status = a.add_approval_status(input.status.clone())?;
1325        let approval = a.add_approval(
1326            m::ApprovalStatusRef::ApprovalStatus(status),
1327            input.level.clone(),
1328        )?;
1329        a.add_applied_approval_assignment(
1330            m::ApprovalRef::Approval(approval),
1331            vec![m::ApprovalItemRef::ProductDefinitionFormation(
1332                self.parts[part.0].formation,
1333            )],
1334        )?;
1335        for (who, role) in &input.approvers {
1336            let role = a.add_approval_role(role.clone())?;
1337            a.add_approval_person_organization(
1338                m::PersonOrganizationSelectRef::PersonAndOrganization(*who),
1339                m::ApprovalRef::Approval(approval),
1340                m::ApprovalRoleRef::ApprovalRole(role),
1341            )?;
1342        }
1343        if let Some(date) = input.date {
1344            let calendar = a.add_calendar_date(date.year, date.day, date.month)?;
1345            let zone = a.add_coordinated_universal_time_offset(0, None, m::AheadOrBehind::Exact)?;
1346            let time = a.add_local_time(
1347                date.hour,
1348                date.minute,
1349                None,
1350                m::CoordinatedUniversalTimeOffsetRef::CoordinatedUniversalTimeOffset(zone),
1351            )?;
1352            let date_time = a.add_date_and_time(
1353                m::DateRef::CalendarDate(calendar),
1354                m::LocalTimeRef::LocalTime(time),
1355            )?;
1356            a.add_approval_date_time(
1357                m::DateTimeSelectRef::DateAndTime(date_time),
1358                m::ApprovalRef::Approval(approval),
1359            )?;
1360        }
1361        Ok(approval)
1362    }
1363
1364    /// Attach an external document reference to the part's version.
1365    ///
1366    /// # Errors
1367    /// Propagates [`AuthorError`] from the strict constructors; the wiring
1368    /// here is fixed, so an error indicates a bug in the builder itself.
1369    pub fn document(&mut self, part: Part, d: &DocumentInput) -> Result<(), AuthorError> {
1370        let a = &mut self.author;
1371        let kind = a.add_document_type(d.kind.clone())?;
1372        let doc = a.add_document(
1373            d.id.clone(),
1374            d.name.clone(),
1375            d.description.clone(),
1376            m::DocumentTypeRef::DocumentType(kind),
1377        )?;
1378        a.add_applied_document_reference(
1379            m::DocumentRef::Document(doc),
1380            String::new(),
1381            vec![m::DocumentReferenceItemRef::ProductDefinitionFormation(
1382                self.parts[part.0].formation,
1383            )],
1384        )?;
1385        Ok(())
1386    }
1387
1388    /// Attach a security classification to the part's version.
1389    ///
1390    /// # Errors
1391    /// Propagates [`AuthorError`] from the strict constructors; the wiring
1392    /// here is fixed, so an error indicates a bug in the builder itself.
1393    pub fn security(
1394        &mut self,
1395        part: Part,
1396        name: &str,
1397        purpose: &str,
1398        level: &str,
1399    ) -> Result<(), AuthorError> {
1400        let a = &mut self.author;
1401        let level = a.add_security_classification_level(level.to_owned())?;
1402        let classification = a.add_security_classification(
1403            name.to_owned(),
1404            purpose.to_owned(),
1405            m::SecurityClassificationLevelRef::SecurityClassificationLevel(level),
1406        )?;
1407        a.add_applied_security_classification_assignment(
1408            m::SecurityClassificationRef::SecurityClassification(classification),
1409            vec![
1410                m::SecurityClassificationItemRef::ProductDefinitionFormation(
1411                    self.parts[part.0].formation,
1412                ),
1413            ],
1414        )?;
1415        Ok(())
1416    }
1417
1418    /// Place `child` inside `parent` at the given frame — one assembly
1419    /// occurrence (the same child may be placed any number of times; each
1420    /// call is one instance). The usage occurrence is created immediately;
1421    /// its shape wiring is bound in [`finish`](Self::finish).
1422    ///
1423    /// # Errors
1424    /// Propagates [`AuthorError`] from the strict constructors; the wiring
1425    /// here is fixed, so an error indicates a bug in the builder itself.
1426    pub fn place(
1427        &mut self,
1428        parent: Part,
1429        child: Part,
1430        at: Frame,
1431    ) -> Result<m::NextAssemblyUsageOccurrenceId, AuthorError> {
1432        let nauo = self.author.add_next_assembly_usage_occurrence(
1433            format!("{}", self.placements.len() + 1),
1434            String::new(),
1435            None,
1436            m::ProductDefinitionOrReferenceRef::ProductDefinition(self.parts[parent.0].definition),
1437            m::ProductDefinitionOrReferenceRef::ProductDefinition(self.parts[child.0].definition),
1438            None,
1439        )?;
1440        let to = self.placement(&at)?;
1441        self.placements.push(PendingPlacement {
1442            nauo,
1443            parent: parent.0,
1444            child: child.0,
1445            to,
1446        });
1447        Ok(nauo)
1448    }
1449
1450    /// Materialize each part's shape representation (its origin placement
1451    /// plus the collected items) with its shape-definition link, add the
1452    /// customary product category, and emit the Part 21 text.
1453    ///
1454    /// # Errors
1455    /// Propagates [`AuthorError`] from the strict constructors; the wiring
1456    /// here is fixed, so an error indicates a bug in the builder itself.
1457    pub fn finish(mut self) -> Result<String, AuthorError> {
1458        let a = &mut self.author;
1459        // Pass 1: one shape representation per part (a part carrying a solid
1460        // is emitted as the customary ADVANCED_BREP_SHAPE_REPRESENTATION),
1461        // kept by part index for the placement wiring below.
1462        let mut part_reps: Vec<m::RepresentationOrRepresentationReferenceRef> =
1463            Vec::with_capacity(self.parts.len());
1464        for part in &self.parts {
1465            let mut items = vec![m::RepresentationItemRef::Axis2Placement3d(part.origin)];
1466            items.extend(part.items.iter().cloned());
1467            let has_solid = part
1468                .items
1469                .iter()
1470                .any(|i| matches!(i, m::RepresentationItemRef::ManifoldSolidBrep(_)));
1471            let (rep, rep_or_ref) = if has_solid {
1472                let id = a.add_advanced_brep_shape_representation(
1473                    String::new(),
1474                    items,
1475                    m::RepresentationContextRef::Complex(self.ctx),
1476                )?;
1477                (
1478                    m::RepresentationRef::AdvancedBrepShapeRepresentation(id),
1479                    m::RepresentationOrRepresentationReferenceRef::AdvancedBrepShapeRepresentation(
1480                        id,
1481                    ),
1482                )
1483            } else {
1484                let id = a.add_shape_representation(
1485                    String::new(),
1486                    items,
1487                    m::RepresentationContextRef::Complex(self.ctx),
1488                )?;
1489                (
1490                    m::RepresentationRef::ShapeRepresentation(id),
1491                    m::RepresentationOrRepresentationReferenceRef::ShapeRepresentation(id),
1492                )
1493            };
1494            a.add_shape_definition_representation(
1495                m::RepresentedDefinitionRef::ProductDefinitionShape(part.shape),
1496                rep,
1497            )?;
1498            part_reps.push(rep_or_ref);
1499
1500            // A part with display meshes gets its own tessellated
1501            // representation alongside the shape one (multi-SDR per shape).
1502            if !part.meshes.is_empty() {
1503                let tsr = a.add_tessellated_shape_representation(
1504                    String::new(),
1505                    part.meshes
1506                        .iter()
1507                        .map(|&t| m::RepresentationItemRef::TessellatedSolid(t))
1508                        .collect(),
1509                    m::RepresentationContextRef::Complex(self.ctx),
1510                )?;
1511                a.add_shape_definition_representation(
1512                    m::RepresentedDefinitionRef::ProductDefinitionShape(part.shape),
1513                    m::RepresentationRef::TessellatedShapeRepresentation(tsr),
1514                )?;
1515            }
1516        }
1517        // Pass 2: assembly placements.
1518        bind_placements(a, &self.parts, &self.placements, &part_reps)?;
1519        if !self.parts.is_empty() {
1520            a.add_product_related_product_category(
1521                "part".to_owned(),
1522                None,
1523                self.parts
1524                    .iter()
1525                    .map(|p| m::ProductRef::Product(p.product))
1526                    .collect(),
1527            )?;
1528        }
1529        // Hidden items (individual styled items and whole layers) collect
1530        // into one INVISIBILITY.
1531        if !self.hidden.is_empty() {
1532            a.add_invisibility(std::mem::take(&mut self.hidden))?;
1533        }
1534        // Styles are anchored in the customary presentation representation.
1535        if !self.styles.is_empty() {
1536            a.add_mechanical_design_geometric_presentation_representation(
1537                String::new(),
1538                self.styles
1539                    .iter()
1540                    .map(|&s| m::RepresentationItemRef::StyledItem(s))
1541                    .collect(),
1542                m::RepresentationContextRef::Complex(self.ctx),
1543            )?;
1544        }
1545        let h = &self.header;
1546        let file_header = FileHeader {
1547            description: h.description.clone().unwrap_or_default(),
1548            file_name: h.file_name.clone().unwrap_or_default(),
1549            time_stamp: h.timestamp.clone().unwrap_or_else(now_iso_utc),
1550            authors: h.authors.clone(),
1551            organizations: h.organizations.clone(),
1552            preprocessor_version: concat!("step-io ", env!("CARGO_PKG_VERSION")).to_owned(),
1553            originating_system: h.originating_system.clone().unwrap_or_default(),
1554            authorisation: h.authorisation.clone().unwrap_or_default(),
1555        };
1556        Ok(self.author.finish_with_header(&file_header))
1557    }
1558}
1559
1560/// Assembly-placement wiring, deferred to `finish`: each occurrence gets its
1561/// placement shape (PDS over the NAUO) bound to an item-defined
1562/// transformation (item 1 = parent origin/from, item 2 = child placement/to)
1563/// through the customary relationship complex (`rep_1` = child, `rep_2` =
1564/// parent).
1565fn bind_placements(
1566    a: &mut Ap242Author,
1567    parts: &[PendingPart],
1568    placements: &[PendingPlacement],
1569    part_reps: &[m::RepresentationOrRepresentationReferenceRef],
1570) -> Result<(), AuthorError> {
1571    for placement in placements {
1572        let pds = a.add_product_definition_shape(
1573            "Placement".to_owned(),
1574            Some("Placement of an item".to_owned()),
1575            m::CharacterizedDefinitionRef::NextAssemblyUsageOccurrence(placement.nauo),
1576        )?;
1577        let idt = a.add_item_defined_transformation(
1578            String::new(),
1579            None,
1580            m::RepresentationItemRef::Axis2Placement3d(parts[placement.parent].origin),
1581            m::RepresentationItemRef::Axis2Placement3d(placement.to),
1582        )?;
1583        let rrwt = a.add_complex(vec![
1584            m::UnitPart::RepresentationRelationship {
1585                name: String::new(),
1586                description: None,
1587                rep_1: part_reps[placement.child].clone(),
1588                rep_2: part_reps[placement.parent].clone(),
1589            },
1590            m::UnitPart::RepresentationRelationshipWithTransformation {
1591                transformation_operator: m::TransformationRef::ItemDefinedTransformation(idt),
1592            },
1593            m::UnitPart::ShapeRepresentationRelationship,
1594        ])?;
1595        a.add_context_dependent_shape_representation(
1596            m::ShapeRepresentationRelationshipRef::Complex(rrwt),
1597            m::ProductDefinitionShapeRef::ProductDefinitionShape(pds),
1598        )?;
1599    }
1600    Ok(())
1601}
1602
1603#[cfg(test)]
1604mod tests {
1605    use super::iso_utc_from_unix;
1606
1607    #[test]
1608    fn iso_utc_from_unix_known_values() {
1609        assert_eq!(iso_utc_from_unix(0), "1970-01-01T00:00:00Z");
1610        // Leap-day boundary: 2024-02-29 23:59:59 UTC.
1611        assert_eq!(iso_utc_from_unix(1_709_251_199), "2024-02-29T23:59:59Z");
1612        // The day after: 2024-03-01 00:00:00 UTC.
1613        assert_eq!(iso_utc_from_unix(1_709_251_200), "2024-03-01T00:00:00Z");
1614    }
1615}