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.into())).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::generated::author::{Ap242Author, AuthorError};
133use crate::generated::model as m;
134use crate::header::FileHeader;
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 (with or without
407/// voids) or one face — the 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 /// A solid with internal voids ([`StepBuilder::solid_with_voids`]).
413 VoidSolid(m::BrepWithVoidsId),
414}
415
416/// Which solid a display mesh tessellates ([`StepBuilder::mesh`]): a plain
417/// [`solid`](StepBuilder::solid) or one with voids
418/// ([`solid_with_voids`](StepBuilder::solid_with_voids)). The id types convert
419/// with [`Into`], so `Some(body.into())` works for either.
420#[derive(Debug, Clone, Copy)]
421pub enum SolidRef {
422 Solid(m::ManifoldSolidBrepId),
423 VoidSolid(m::BrepWithVoidsId),
424}
425
426impl From<m::ManifoldSolidBrepId> for SolidRef {
427 fn from(id: m::ManifoldSolidBrepId) -> Self {
428 SolidRef::Solid(id)
429 }
430}
431
432impl From<m::BrepWithVoidsId> for SolidRef {
433 fn from(id: m::BrepWithVoidsId) -> Self {
434 SolidRef::VoidSolid(id)
435 }
436}
437
438/// Which way the face normals of the void shells passed to
439/// [`StepBuilder::solid_with_voids`] point. The caller declares this so
440/// step-io can orient each cavity's `ORIENTED_CLOSED_SHELL` correctly without
441/// evaluating geometry. See that method for how to choose.
442#[derive(Debug, Clone, Copy)]
443pub enum VoidShellNormals {
444 /// Normals point away from the solid material — for a cavity, into the
445 /// empty hole. This is the same rule the outer shell follows, the
446 /// validator- and mass-property-friendly orientation, and what most
447 /// kernels emit for a cavity (a reversed shell). Kept as authored,
448 /// written as `ORIENTED_CLOSED_SHELL(..., .T.)`. The right choice when unsure.
449 AwayFromMaterial,
450 /// Normals point into the solid material — a cavity wound like an ordinary
451 /// outward-facing solid box dropped into the hole. step-io reverses it,
452 /// written as `ORIENTED_CLOSED_SHELL(..., .F.)`.
453 TowardMaterial,
454}
455
456/// Handle for a vertex created by [`StepBuilder::vertex`] — an index into
457/// the builder that created it (a different builder's handle panics or
458/// targets the wrong vertex). The builder keeps the vertex position so
459/// straight edges can derive their line geometry.
460#[derive(Debug, Copy, Clone, PartialEq, Eq)]
461pub struct Vertex(usize);
462
463/// Handle for a part created by [`StepBuilder::part`] — an index into the
464/// builder that created it. Passing it to a different builder panics (out
465/// of range) or silently targets the wrong part (in range).
466#[derive(Debug, Copy, Clone, PartialEq, Eq)]
467pub struct Part(usize);
468
469/// A part's product chain plus the shape items collected for it; the shape
470/// representation itself is materialized in [`StepBuilder::finish`] because
471/// representation items are fixed at insertion.
472struct PendingPart {
473 product: m::ProductId,
474 formation: m::ProductDefinitionFormationId,
475 definition: m::ProductDefinitionId,
476 shape: m::ProductDefinitionShapeId,
477 origin: m::Axis2Placement3dId,
478 items: Vec<m::RepresentationItemRef>,
479 meshes: Vec<m::TessellatedSolidId>,
480}
481
482/// One [`StepBuilder::place`] call: the eagerly-created usage occurrence
483/// plus the wiring deferred to [`StepBuilder::finish`] (the transformation
484/// relationship needs both parts' shape representations, which exist only
485/// then).
486struct PendingPlacement {
487 nauo: m::NextAssemblyUsageOccurrenceId,
488 parent: usize,
489 child: usize,
490 to: m::Axis2Placement3dId,
491}
492
493/// Ergonomic writer for the standard AP242 file shape: shared contexts and
494/// units up front, one product chain per [`part`](Self::part), everything
495/// bound and emitted by [`finish`](Self::finish).
496pub struct StepBuilder {
497 author: Ap242Author,
498 ctx: m::ComplexUnitId,
499 product_context: m::ProductContextId,
500 pd_context: m::ProductDefinitionContextId,
501 parts: Vec<PendingPart>,
502 placements: Vec<PendingPlacement>,
503 vertices: Vec<(m::VertexPointId, [f64; 3])>,
504 styles: Vec<m::StyledItemId>,
505 hidden: Vec<m::InvisibleItemRef>,
506 header: HeaderInput,
507}
508
509impl StepBuilder {
510 /// Set up the shared skeleton with the default units (millimetre,
511 /// 1e-7 uncertainty) — see [`new_with`](Self::new_with) to choose.
512 ///
513 /// # Errors
514 /// Propagates [`AuthorError`] from the strict constructors; the wiring
515 /// here is fixed, so an error indicates a bug in the builder itself.
516 pub fn new() -> Result<Self, AuthorError> {
517 Self::new_with(&UnitsInput::default())
518 }
519
520 /// Set up the shared skeleton: application context + protocol definition
521 /// (the AP242 identity values), product contexts, and the geometric
522 /// context complex carrying the chosen SI length unit, radian,
523 /// steradian, and the given length uncertainty.
524 ///
525 /// # Errors
526 /// Propagates [`AuthorError`] from the strict constructors; the wiring
527 /// here is fixed, so an error indicates a bug in the builder itself.
528 pub fn new_with(units: &UnitsInput) -> Result<Self, AuthorError> {
529 let mut a = Ap242Author::new();
530
531 let ac = a.add_application_context("managed model based 3d engineering".to_owned())?;
532 a.add_application_protocol_definition(
533 "international standard".to_owned(),
534 "ap242_managed_model_based_3d_engineering_mim_lf".to_owned(),
535 2011,
536 m::ApplicationContextRef::ApplicationContext(ac),
537 )?;
538 let product_context = a.add_product_context(
539 String::new(),
540 m::ApplicationContextRef::ApplicationContext(ac),
541 "mechanical".to_owned(),
542 )?;
543 let pd_context = a.add_product_definition_context(
544 "part definition".to_owned(),
545 m::ApplicationContextRef::ApplicationContext(ac),
546 "design".to_owned(),
547 )?;
548
549 let length_prefix = match units.length {
550 LengthUnit::Millimetre => Some(m::SiPrefix::Milli),
551 LengthUnit::Metre => None,
552 };
553 let mm = a.add_complex(vec![
554 m::UnitPart::LengthUnit,
555 m::UnitPart::NamedUnit { dimensions: None },
556 m::UnitPart::SiUnit {
557 prefix: length_prefix,
558 name: m::SiUnitName::Metre,
559 },
560 ])?;
561 let rad = a.add_complex(vec![
562 m::UnitPart::NamedUnit { dimensions: None },
563 m::UnitPart::PlaneAngleUnit,
564 m::UnitPart::SiUnit {
565 prefix: None,
566 name: m::SiUnitName::Radian,
567 },
568 ])?;
569 let sr = a.add_complex(vec![
570 m::UnitPart::NamedUnit { dimensions: None },
571 m::UnitPart::SiUnit {
572 prefix: None,
573 name: m::SiUnitName::Steradian,
574 },
575 m::UnitPart::SolidAngleUnit,
576 ])?;
577 let uncertainty = a.add_uncertainty_measure_with_unit(
578 m::MeasureValue {
579 type_name: Some("LENGTH_MEASURE".to_owned()),
580 value: m::MeasureScalar::Real(units.uncertainty),
581 },
582 m::UnitRef::Complex(mm),
583 "distance_accuracy_value".to_owned(),
584 Some("confusion accuracy".to_owned()),
585 )?;
586 let ctx = a.add_complex(vec![
587 m::UnitPart::GeometricRepresentationContext {
588 coordinate_space_dimension: 3,
589 },
590 m::UnitPart::GlobalUncertaintyAssignedContext {
591 uncertainty: vec![
592 m::UncertaintyMeasureWithUnitRef::UncertaintyMeasureWithUnit(uncertainty),
593 ],
594 },
595 m::UnitPart::GlobalUnitAssignedContext {
596 units: vec![
597 m::UnitRef::Complex(mm),
598 m::UnitRef::Complex(rad),
599 m::UnitRef::Complex(sr),
600 ],
601 },
602 m::UnitPart::RepresentationContext {
603 context_identifier: String::new(),
604 context_type: "3D".to_owned(),
605 },
606 ])?;
607
608 Ok(Self {
609 author: a,
610 ctx,
611 product_context,
612 pd_context,
613 parts: Vec::new(),
614 placements: Vec::new(),
615 vertices: Vec::new(),
616 styles: Vec::new(),
617 hidden: Vec::new(),
618 header: HeaderInput::default(),
619 })
620 }
621
622 /// Set the Part 21 HEADER fields (file name, description, authors,
623 /// organizations, originating system, authorisation, timestamp) applied
624 /// on [`finish`](Self::finish). May be called any time before `finish`;
625 /// the last call wins. Without it the header carries only the automatic
626 /// timestamp and preprocessor stamp.
627 pub fn header(&mut self, h: &HeaderInput) {
628 self.header = h.clone();
629 }
630
631 /// Escape hatch to the strict low-level layer — the write-side
632 /// counterpart of the read side's `Scene::model()`. Entities the builder
633 /// does not cover go through [`Ap242Author`]'s generated constructors
634 /// directly; ids cross freely in both directions (builder-produced ids
635 /// feed low-level calls and vice versa — same model, same validation).
636 ///
637 /// Only borrowed access is offered: taking the author out would bypass
638 /// [`finish`](Self::finish), which still has to materialize the pending
639 /// shape representations, placements, style anchors, category, and
640 /// header.
641 pub fn author(&mut self) -> &mut Ap242Author {
642 &mut self.author
643 }
644
645 /// Colour `target` (a solid or one face), optionally with a transparency
646 /// (`0.0` = opaque, `1.0` = fully transparent). Styling the same target
647 /// again adds another style record; readers use the first they find.
648 ///
649 /// # Errors
650 /// An id that did not come from this builder (another builder's, or out
651 /// of range) fails validation with [`AuthorError`]; beyond that the
652 /// wiring is fixed, so an error indicates a bug in the builder itself.
653 pub fn style(
654 &mut self,
655 target: StyleTarget,
656 color: Rgb,
657 transparency: Option<f64>,
658 ) -> Result<m::StyledItemId, AuthorError> {
659 let a = &mut self.author;
660 let rgb = a.add_colour_rgb(String::new(), color.red, color.green, color.blue)?;
661 // Colour alone rides the customary fill-area chain; with a
662 // transparency the rendering form carries both.
663 let element = if let Some(t) = transparency {
664 let transparent = a.add_surface_style_transparent(t)?;
665 let rendering = a.add_surface_style_rendering_with_properties(
666 m::ShadingSurfaceMethod::NormalShading,
667 m::ColourRef::ColourRgb(rgb),
668 vec![m::RenderingPropertiesSelectRef::SurfaceStyleTransparent(
669 transparent,
670 )],
671 )?;
672 m::SurfaceStyleElementSelectRef::SurfaceStyleRenderingWithProperties(rendering)
673 } else {
674 let fill_colour =
675 a.add_fill_area_style_colour(String::new(), m::ColourRef::ColourRgb(rgb))?;
676 let fill_style = a.add_fill_area_style(
677 String::new(),
678 vec![m::FillStyleSelectRef::FillAreaStyleColour(fill_colour)],
679 )?;
680 let fill_area =
681 a.add_surface_style_fill_area(m::FillAreaStyleRef::FillAreaStyle(fill_style))?;
682 m::SurfaceStyleElementSelectRef::SurfaceStyleFillArea(fill_area)
683 };
684 let side_style = a.add_surface_side_style(String::new(), vec![element])?;
685 let usage = a.add_surface_style_usage(
686 m::SurfaceSide::Both,
687 m::SurfaceSideStyleSelectRef::SurfaceSideStyle(side_style),
688 )?;
689 let assignment = a.add_presentation_style_assignment(vec![
690 m::PresentationStyleSelectRef::SurfaceStyleUsage(usage),
691 ])?;
692 let item = match target {
693 StyleTarget::Face(f) => m::StyledItemTargetRef::AdvancedFace(f),
694 StyleTarget::Solid(s) => m::StyledItemTargetRef::ManifoldSolidBrep(s),
695 StyleTarget::VoidSolid(s) => m::StyledItemTargetRef::BrepWithVoids(s),
696 };
697 let styled = a.add_styled_item(
698 String::new(),
699 vec![m::PresentationStyleAssignmentRef::PresentationStyleAssignment(assignment)],
700 item,
701 )?;
702 self.styles.push(styled);
703 Ok(styled)
704 }
705
706 /// Assign `targets` to a named presentation layer; the returned id can
707 /// be fed to [`hide_layer`](Self::hide_layer) to switch the whole layer
708 /// off by default.
709 ///
710 /// # Errors
711 /// An id that did not come from this builder (another builder's, or out
712 /// of range) fails validation with [`AuthorError`]; beyond that the
713 /// wiring is fixed, so an error indicates a bug in the builder itself.
714 pub fn layer(
715 &mut self,
716 name: &str,
717 targets: Vec<StyleTarget>,
718 ) -> Result<m::PresentationLayerAssignmentId, AuthorError> {
719 let items = targets
720 .into_iter()
721 .map(|t| match t {
722 StyleTarget::Face(f) => m::LayeredItemRef::AdvancedFace(f),
723 StyleTarget::Solid(s) => m::LayeredItemRef::ManifoldSolidBrep(s),
724 StyleTarget::VoidSolid(s) => m::LayeredItemRef::BrepWithVoids(s),
725 })
726 .collect();
727 self.author
728 .add_presentation_layer_assignment(name.to_owned(), String::new(), items)
729 }
730
731 /// Mark `target` invisible by default. Implemented the way STEP requires
732 /// it: an `INVISIBILITY` cannot point at geometry directly, so the
733 /// builder attaches an empty (`.NULL.`-styled) styled item to the target
734 /// and registers that — colour styles on the same target are unaffected.
735 ///
736 /// # Errors
737 /// An id that did not come from this builder (another builder's, or out
738 /// of range) fails validation with [`AuthorError`]; beyond that the
739 /// wiring is fixed, so an error indicates a bug in the builder itself.
740 pub fn hide(&mut self, target: StyleTarget) -> Result<(), AuthorError> {
741 let a = &mut self.author;
742 let assignment =
743 a.add_presentation_style_assignment(vec![m::PresentationStyleSelectRef::NullStyle(
744 m::NullStyle::Null,
745 )])?;
746 let item = match target {
747 StyleTarget::Face(f) => m::StyledItemTargetRef::AdvancedFace(f),
748 StyleTarget::Solid(s) => m::StyledItemTargetRef::ManifoldSolidBrep(s),
749 StyleTarget::VoidSolid(s) => m::StyledItemTargetRef::BrepWithVoids(s),
750 };
751 let styled = a.add_styled_item(
752 String::new(),
753 vec![m::PresentationStyleAssignmentRef::PresentationStyleAssignment(assignment)],
754 item,
755 )?;
756 self.styles.push(styled);
757 self.hidden.push(m::InvisibleItemRef::StyledItem(styled));
758 Ok(())
759 }
760
761 /// Switch a whole layer (from [`layer`](Self::layer)) invisible by
762 /// default. Infallible at call time — a foreign or stale id is caught by
763 /// the `INVISIBILITY` validation inside [`finish`](Self::finish).
764 pub fn hide_layer(&mut self, layer: m::PresentationLayerAssignmentId) {
765 self.hidden
766 .push(m::InvisibleItemRef::PresentationLayerAssignment(layer));
767 }
768
769 /// `AXIS2_PLACEMENT_3D` from a [`Frame`].
770 fn placement(&mut self, f: &Frame) -> Result<m::Axis2Placement3dId, AuthorError> {
771 let a = &mut self.author;
772 let location = a.add_cartesian_point(String::new(), f.origin.to_vec())?;
773 let axis = a.add_direction(String::new(), f.axis.to_vec())?;
774 let ref_direction = a.add_direction(String::new(), f.ref_dir.to_vec())?;
775 a.add_axis2_placement3d(
776 String::new(),
777 m::CartesianPointRef::CartesianPoint(location),
778 Some(m::DirectionRef::Direction(axis)),
779 Some(m::DirectionRef::Direction(ref_direction)),
780 )
781 }
782
783 /// One `CARTESIAN_POINT` ref per control point.
784 fn control_points(
785 &mut self,
786 row: &[[f64; 3]],
787 ) -> Result<Vec<m::CartesianPointRef>, AuthorError> {
788 row.iter()
789 .map(|p| {
790 self.author
791 .add_cartesian_point(String::new(), p.to_vec())
792 .map(m::CartesianPointRef::CartesianPoint)
793 })
794 .collect()
795 }
796
797 /// Geometry for a swept-surface profile (also backs the ellipse edge).
798 fn profile_geometry(&mut self, profile: &ProfileInput) -> Result<m::CurveRef, AuthorError> {
799 match profile {
800 ProfileInput::Line(point, dir) => {
801 let a = &mut self.author;
802 let pnt = a.add_cartesian_point(String::new(), point.to_vec())?;
803 let orientation = a.add_direction(String::new(), dir.to_vec())?;
804 let vector =
805 a.add_vector(String::new(), m::DirectionRef::Direction(orientation), 1.0)?;
806 let line = a.add_line(
807 String::new(),
808 m::CartesianPointRef::CartesianPoint(pnt),
809 m::VectorRef::Vector(vector),
810 )?;
811 Ok(m::CurveRef::Line(line))
812 }
813 ProfileInput::Circle(frame, radius) => {
814 let position = self.placement(frame)?;
815 let circle = self.author.add_circle(
816 String::new(),
817 m::Axis2PlacementRef::Axis2Placement3d(position),
818 *radius,
819 )?;
820 Ok(m::CurveRef::Circle(circle))
821 }
822 ProfileInput::Ellipse(frame, semi_1, semi_2) => {
823 let position = self.placement(frame)?;
824 let ellipse = self.author.add_ellipse(
825 String::new(),
826 m::Axis2PlacementRef::Axis2Placement3d(position),
827 *semi_1,
828 *semi_2,
829 )?;
830 Ok(m::CurveRef::Ellipse(ellipse))
831 }
832 ProfileInput::Nurbs(curve) => self.nurbs_curve_geometry(curve),
833 }
834 }
835
836 /// NURBS curve geometry: non-rational (all weights `1.0`) as a plain
837 /// `B_SPLINE_CURVE_WITH_KNOTS`, rational as the customary multi-part
838 /// complex instance.
839 // Exact comparison intended: only weights that are exactly 1.0 take the
840 // non-rational form; anything else keeps its value in weights_data.
841 #[allow(clippy::float_cmp)]
842 fn nurbs_curve_geometry(&mut self, c: &NurbsCurve) -> Result<m::CurveRef, AuthorError> {
843 let cps = self.control_points(&c.control_points)?;
844 let (knots, knot_multiplicities) = compress_knots(&c.knots);
845 let degree = i64::try_from(c.degree).unwrap_or(i64::MAX);
846 if c.weights.iter().all(|w| *w == 1.0) {
847 let id = self.author.add_b_spline_curve_with_knots(
848 String::new(),
849 degree,
850 cps,
851 m::BSplineCurveForm::Unspecified,
852 m::Logical::Unknown,
853 m::Logical::Unknown,
854 knot_multiplicities,
855 knots,
856 m::KnotType::Unspecified,
857 )?;
858 Ok(m::CurveRef::BSplineCurveWithKnots(id))
859 } else {
860 let id = self.author.add_complex(vec![
861 m::UnitPart::BoundedCurve,
862 m::UnitPart::BSplineCurve {
863 degree,
864 control_points_list: cps,
865 curve_form: m::BSplineCurveForm::Unspecified,
866 closed_curve: m::Logical::Unknown,
867 self_intersect: m::Logical::Unknown,
868 },
869 m::UnitPart::BSplineCurveWithKnots {
870 knot_multiplicities,
871 knots,
872 knot_spec: m::KnotType::Unspecified,
873 },
874 m::UnitPart::Curve,
875 m::UnitPart::GeometricRepresentationItem,
876 m::UnitPart::RationalBSplineCurve {
877 weights_data: c.weights.clone(),
878 },
879 m::UnitPart::RepresentationItem {
880 name: String::new(),
881 },
882 ])?;
883 Ok(m::CurveRef::Complex(id))
884 }
885 }
886
887 /// NURBS surface geometry; see [`Self::nurbs_curve_geometry`].
888 #[allow(clippy::float_cmp)] // exact 1.0 check, as in nurbs_curve_geometry
889 fn nurbs_surface_geometry(&mut self, s: &NurbsSurface) -> Result<m::SurfaceRef, AuthorError> {
890 let mut cps = Vec::with_capacity(s.control_points.len());
891 for row in &s.control_points {
892 cps.push(self.control_points(row)?);
893 }
894 let (u_knots, u_multiplicities) = compress_knots(&s.knots_u);
895 let (v_knots, v_multiplicities) = compress_knots(&s.knots_v);
896 let u_degree = i64::try_from(s.degree_u).unwrap_or(i64::MAX);
897 let v_degree = i64::try_from(s.degree_v).unwrap_or(i64::MAX);
898 if s.weights.iter().flatten().all(|w| *w == 1.0) {
899 let id = self.author.add_b_spline_surface_with_knots(
900 String::new(),
901 u_degree,
902 v_degree,
903 cps,
904 m::BSplineSurfaceForm::Unspecified,
905 m::Logical::Unknown,
906 m::Logical::Unknown,
907 m::Logical::Unknown,
908 u_multiplicities,
909 v_multiplicities,
910 u_knots,
911 v_knots,
912 m::KnotType::Unspecified,
913 )?;
914 Ok(m::SurfaceRef::BSplineSurfaceWithKnots(id))
915 } else {
916 let id = self.author.add_complex(vec![
917 m::UnitPart::BoundedSurface,
918 m::UnitPart::BSplineSurface {
919 u_degree,
920 v_degree,
921 control_points_list: cps,
922 surface_form: m::BSplineSurfaceForm::Unspecified,
923 u_closed: m::Logical::Unknown,
924 v_closed: m::Logical::Unknown,
925 self_intersect: m::Logical::Unknown,
926 },
927 m::UnitPart::BSplineSurfaceWithKnots {
928 u_multiplicities,
929 v_multiplicities,
930 u_knots,
931 v_knots,
932 knot_spec: m::KnotType::Unspecified,
933 },
934 m::UnitPart::GeometricRepresentationItem,
935 m::UnitPart::RationalBSplineSurface {
936 weights_data: s.weights.clone(),
937 },
938 m::UnitPart::RepresentationItem {
939 name: String::new(),
940 },
941 m::UnitPart::Surface,
942 ])?;
943 Ok(m::SurfaceRef::Complex(id))
944 }
945 }
946
947 /// Add a topological vertex at `p`.
948 ///
949 /// # Errors
950 /// Propagates [`AuthorError`] from the strict constructors; the wiring
951 /// here is fixed, so an error indicates a bug in the builder itself.
952 pub fn vertex(&mut self, p: [f64; 3]) -> Result<Vertex, AuthorError> {
953 let point = self.author.add_cartesian_point(String::new(), p.to_vec())?;
954 let vp = self
955 .author
956 .add_vertex_point(String::new(), m::PointRef::CartesianPoint(point))?;
957 self.vertices.push((vp, p));
958 Ok(Vertex(self.vertices.len() - 1))
959 }
960
961 /// Add an edge between two vertices over the given curve. A straight
962 /// edge derives its line from the vertex positions (a zero-length
963 /// segment gets a placeholder direction — degenerate geometry is the
964 /// caller's responsibility); a circle with `from == to` is a closed
965 /// full-circle edge.
966 ///
967 /// # Errors
968 /// Propagates [`AuthorError`] from the strict constructors; the wiring
969 /// here is fixed, so an error indicates a bug in the builder itself.
970 pub fn edge(
971 &mut self,
972 from: Vertex,
973 to: Vertex,
974 curve: CurveInput,
975 ) -> Result<m::EdgeCurveId, AuthorError> {
976 let (start_vertex, start) = self.vertices[from.0];
977 let (end_vertex, end) = self.vertices[to.0];
978 let geometry = match curve {
979 CurveInput::Line => {
980 let delta = [end[0] - start[0], end[1] - start[1], end[2] - start[2]];
981 let len = (delta[0] * delta[0] + delta[1] * delta[1] + delta[2] * delta[2]).sqrt();
982 let dir = if len < 1e-12 {
983 [0.0, 0.0, 1.0]
984 } else {
985 [delta[0] / len, delta[1] / len, delta[2] / len]
986 };
987 let a = &mut self.author;
988 let pnt = a.add_cartesian_point(String::new(), start.to_vec())?;
989 let orientation = a.add_direction(String::new(), dir.to_vec())?;
990 let vector =
991 a.add_vector(String::new(), m::DirectionRef::Direction(orientation), len)?;
992 let line = a.add_line(
993 String::new(),
994 m::CartesianPointRef::CartesianPoint(pnt),
995 m::VectorRef::Vector(vector),
996 )?;
997 m::CurveRef::Line(line)
998 }
999 CurveInput::Circle(frame, radius) => {
1000 let position = self.placement(&frame)?;
1001 let circle = self.author.add_circle(
1002 String::new(),
1003 m::Axis2PlacementRef::Axis2Placement3d(position),
1004 radius,
1005 )?;
1006 m::CurveRef::Circle(circle)
1007 }
1008 CurveInput::Ellipse(frame, semi_1, semi_2) => {
1009 self.profile_geometry(&ProfileInput::Ellipse(frame, semi_1, semi_2))?
1010 }
1011 CurveInput::Polyline(points) => {
1012 let refs = self.control_points(&points)?;
1013 m::CurveRef::Polyline(self.author.add_polyline(String::new(), refs)?)
1014 }
1015 CurveInput::Nurbs(curve) => self.nurbs_curve_geometry(&curve)?,
1016 };
1017 self.author.add_edge_curve(
1018 String::new(),
1019 m::VertexRef::VertexPoint(start_vertex),
1020 m::VertexRef::VertexPoint(end_vertex),
1021 geometry,
1022 true,
1023 )
1024 }
1025
1026 /// Emit the geometry entity for a [`SurfaceInput`].
1027 fn surface_geometry(&mut self, surface: SurfaceInput) -> Result<m::SurfaceRef, AuthorError> {
1028 Ok(match surface {
1029 SurfaceInput::Plane(frame) => {
1030 let position = self.placement(&frame)?;
1031 let plane = self.author.add_plane(
1032 String::new(),
1033 m::Axis2Placement3dRef::Axis2Placement3d(position),
1034 )?;
1035 m::SurfaceRef::Plane(plane)
1036 }
1037 SurfaceInput::Cylinder(frame, radius) => {
1038 let position = self.placement(&frame)?;
1039 let cyl = self.author.add_cylindrical_surface(
1040 String::new(),
1041 m::Axis2Placement3dRef::Axis2Placement3d(position),
1042 radius,
1043 )?;
1044 m::SurfaceRef::CylindricalSurface(cyl)
1045 }
1046 SurfaceInput::Sphere(frame, radius) => {
1047 let position = self.placement(&frame)?;
1048 let sphere = self.author.add_spherical_surface(
1049 String::new(),
1050 m::Axis2Placement3dRef::Axis2Placement3d(position),
1051 radius,
1052 )?;
1053 m::SurfaceRef::SphericalSurface(sphere)
1054 }
1055 SurfaceInput::Torus(frame, major_radius, minor_radius) => {
1056 let position = self.placement(&frame)?;
1057 let torus = self.author.add_toroidal_surface(
1058 String::new(),
1059 m::Axis2Placement3dRef::Axis2Placement3d(position),
1060 major_radius,
1061 minor_radius,
1062 )?;
1063 m::SurfaceRef::ToroidalSurface(torus)
1064 }
1065 SurfaceInput::Cone(frame, radius, semi_angle) => {
1066 let position = self.placement(&frame)?;
1067 let cone = self.author.add_conical_surface(
1068 String::new(),
1069 m::Axis2Placement3dRef::Axis2Placement3d(position),
1070 radius,
1071 semi_angle,
1072 )?;
1073 m::SurfaceRef::ConicalSurface(cone)
1074 }
1075 SurfaceInput::LinearExtrusion(profile, sweep) => {
1076 let swept_curve = self.profile_geometry(&profile)?;
1077 let len = (sweep[0] * sweep[0] + sweep[1] * sweep[1] + sweep[2] * sweep[2]).sqrt();
1078 let dir = if len < 1e-12 {
1079 [0.0, 0.0, 1.0]
1080 } else {
1081 [sweep[0] / len, sweep[1] / len, sweep[2] / len]
1082 };
1083 let a = &mut self.author;
1084 let orientation = a.add_direction(String::new(), dir.to_vec())?;
1085 let vector =
1086 a.add_vector(String::new(), m::DirectionRef::Direction(orientation), len)?;
1087 let surf = a.add_surface_of_linear_extrusion(
1088 String::new(),
1089 swept_curve,
1090 m::VectorRef::Vector(vector),
1091 )?;
1092 m::SurfaceRef::SurfaceOfLinearExtrusion(surf)
1093 }
1094 SurfaceInput::Revolution(profile, origin, axis) => {
1095 let swept_curve = self.profile_geometry(&profile)?;
1096 let a = &mut self.author;
1097 let location = a.add_cartesian_point(String::new(), origin.to_vec())?;
1098 let axis_dir = a.add_direction(String::new(), axis.to_vec())?;
1099 let position = a.add_axis1_placement(
1100 String::new(),
1101 m::CartesianPointRef::CartesianPoint(location),
1102 Some(m::DirectionRef::Direction(axis_dir)),
1103 )?;
1104 let surf = a.add_surface_of_revolution(
1105 String::new(),
1106 swept_curve,
1107 m::Axis1PlacementRef::Axis1Placement(position),
1108 )?;
1109 m::SurfaceRef::SurfaceOfRevolution(surf)
1110 }
1111 SurfaceInput::Nurbs(surface) => self.nurbs_surface_geometry(&surface)?,
1112 })
1113 }
1114
1115 /// Add a face over an analytic surface, bounded by the given loops.
1116 ///
1117 /// # Errors
1118 /// An id that did not come from this builder (another builder's, or out
1119 /// of range) fails validation with [`AuthorError`]; beyond that the
1120 /// wiring is fixed, so an error indicates a bug in the builder itself.
1121 pub fn face(
1122 &mut self,
1123 surface: SurfaceInput,
1124 same_sense: bool,
1125 bounds: Vec<FaceBoundInput>,
1126 ) -> Result<m::AdvancedFaceId, AuthorError> {
1127 let face_geometry = self.surface_geometry(surface)?;
1128 let mut face_bounds = Vec::with_capacity(bounds.len());
1129 for bound in bounds {
1130 let mut edge_list = Vec::with_capacity(bound.edges.len());
1131 for (edge, forward) in bound.edges {
1132 let oe = self.author.add_oriented_edge(
1133 String::new(),
1134 m::EdgeRef::EdgeCurve(edge),
1135 forward,
1136 )?;
1137 edge_list.push(m::OrientedEdgeRef::OrientedEdge(oe));
1138 }
1139 let el = self.author.add_edge_loop(String::new(), edge_list)?;
1140 let fb = if bound.outer {
1141 m::FaceBoundRef::FaceOuterBound(self.author.add_face_outer_bound(
1142 String::new(),
1143 m::LoopRef::EdgeLoop(el),
1144 bound.orientation,
1145 )?)
1146 } else {
1147 m::FaceBoundRef::FaceBound(self.author.add_face_bound(
1148 String::new(),
1149 m::LoopRef::EdgeLoop(el),
1150 bound.orientation,
1151 )?)
1152 };
1153 face_bounds.push(fb);
1154 }
1155 self.author
1156 .add_advanced_face(String::new(), face_bounds, face_geometry, same_sense)
1157 }
1158
1159 /// Close the faces into a shell and add the solid to `part`; the solid
1160 /// lands in the part's shape representation on [`finish`](Self::finish)
1161 /// (as an `ADVANCED_BREP_SHAPE_REPRESENTATION`).
1162 ///
1163 /// # Errors
1164 /// An id that did not come from this builder (another builder's, or out
1165 /// of range) fails validation with [`AuthorError`]; beyond that the
1166 /// wiring is fixed, so an error indicates a bug in the builder itself.
1167 pub fn solid(
1168 &mut self,
1169 part: Part,
1170 name: &str,
1171 faces: Vec<m::AdvancedFaceId>,
1172 ) -> Result<m::ManifoldSolidBrepId, AuthorError> {
1173 let shell = self.author.add_closed_shell(
1174 String::new(),
1175 faces.into_iter().map(m::FaceRef::AdvancedFace).collect(),
1176 )?;
1177 let solid = self
1178 .author
1179 .add_manifold_solid_brep(name.to_owned(), m::ClosedShellRef::ClosedShell(shell))?;
1180 self.parts[part.0]
1181 .items
1182 .push(m::RepresentationItemRef::ManifoldSolidBrep(solid));
1183 Ok(solid)
1184 }
1185
1186 /// Close `outer_faces` into the outer shell and each group in `voids` into
1187 /// an internal cavity shell, then add a solid with internal voids
1188 /// (`BREP_WITH_VOIDS`) to `part` — a cavity-bearing body such as a hollow
1189 /// casting. The solid lands in the part's shape representation on
1190 /// [`finish`](Self::finish) (as an `ADVANCED_BREP_SHAPE_REPRESENTATION`,
1191 /// like [`solid`](Self::solid)).
1192 ///
1193 /// `voids` must be non-empty (a solid with no voids is a plain
1194 /// [`solid`](Self::solid)); each inner group is one closed cavity shell.
1195 ///
1196 /// # Void shell orientation (`normals`)
1197 ///
1198 /// STEP orients every bounding shell so its face normals point away from
1199 /// the solid material: outward into free space for the outer shell, and
1200 /// into the empty cavity for a void. Each void group is wrapped in an
1201 /// [`ORIENTED_CLOSED_SHELL`](m::OrientedClosedShell); `normals` tells
1202 /// step-io whether the faces you pass already follow that rule or must be
1203 /// flipped to it.
1204 ///
1205 /// - [`AwayFromMaterial`](VoidShellNormals::AwayFromMaterial): the faces
1206 /// already point out of the solid (into the cavity) — the same rule your
1207 /// outer shell follows. Natural, and what most kernels produce when they
1208 /// emit a cavity as a reversed shell; kept as authored (`.T.`). Use this
1209 /// if your kernel orients cavities consistently with the outer shell.
1210 /// When in doubt, this is the right choice.
1211 /// - [`TowardMaterial`](VoidShellNormals::TowardMaterial): the faces point
1212 /// into the material — you built the cavity like an ordinary
1213 /// outward-facing box (normals facing outward from the cavity volume,
1214 /// toward the surrounding solid). step-io reverses them (`.F.`).
1215 ///
1216 /// Quick test: look along a cavity wall's normal. Into the empty hole →
1217 /// `AwayFromMaterial`; into the surrounding solid → `TowardMaterial`.
1218 /// step-io does not evaluate geometry — it trusts this declaration, and the
1219 /// wrong value inverts the void (validators and mass-property sign
1220 /// conventions will reject it).
1221 ///
1222 /// # Errors
1223 /// Empty `voids`, or empty face lists, fail validation with
1224 /// [`AuthorError`] (the strict `CLOSED_SHELL` / `BREP_WITH_VOIDS`
1225 /// cardinality checks). An id that did not come from this builder fails the
1226 /// same way; beyond that the wiring is fixed, so an error indicates a bug
1227 /// in the builder itself.
1228 pub fn solid_with_voids(
1229 &mut self,
1230 part: Part,
1231 name: &str,
1232 outer_faces: Vec<m::AdvancedFaceId>,
1233 voids: Vec<Vec<m::AdvancedFaceId>>,
1234 normals: VoidShellNormals,
1235 ) -> Result<m::BrepWithVoidsId, AuthorError> {
1236 let outer = self.author.add_closed_shell(
1237 String::new(),
1238 outer_faces
1239 .into_iter()
1240 .map(m::FaceRef::AdvancedFace)
1241 .collect(),
1242 )?;
1243 // `.T.` keeps the faces as authored, `.F.` reverses them; see
1244 // `conditional_reverse` in the schema.
1245 let orientation = match normals {
1246 VoidShellNormals::AwayFromMaterial => true, // .T. — as authored (natural)
1247 VoidShellNormals::TowardMaterial => false, // .F. — topology_reversed
1248 };
1249 let mut void_refs = Vec::with_capacity(voids.len());
1250 for void_faces in voids {
1251 let shell = self.author.add_closed_shell(
1252 String::new(),
1253 void_faces
1254 .into_iter()
1255 .map(m::FaceRef::AdvancedFace)
1256 .collect(),
1257 )?;
1258 let oriented = self.author.add_oriented_closed_shell(
1259 String::new(),
1260 m::ClosedShellRef::ClosedShell(shell),
1261 orientation,
1262 )?;
1263 void_refs.push(m::OrientedClosedShellRef::OrientedClosedShell(oriented));
1264 }
1265 let solid = self.author.add_brep_with_voids(
1266 name.to_owned(),
1267 m::ClosedShellRef::ClosedShell(outer),
1268 void_refs,
1269 )?;
1270 self.parts[part.0]
1271 .items
1272 .push(m::RepresentationItemRef::BrepWithVoids(solid));
1273 Ok(solid)
1274 }
1275
1276 /// Add one part: the full product chain (product → formation →
1277 /// definition → definition shape) plus its origin placement. Shape items
1278 /// collected for the part are bound into its shape representation by
1279 /// [`finish`](Self::finish).
1280 ///
1281 /// # Errors
1282 /// Propagates [`AuthorError`] from the strict constructors; the wiring
1283 /// here is fixed, so an error indicates a bug in the builder itself.
1284 pub fn part(&mut self, name: &str) -> Result<Part, AuthorError> {
1285 self.part_with(name, &PartOptions::default())
1286 }
1287
1288 /// [`part`](Self::part) with metadata overrides: part number, version
1289 /// label, and the description fields.
1290 ///
1291 /// # Errors
1292 /// Propagates [`AuthorError`] from the strict constructors; the wiring
1293 /// here is fixed, so an error indicates a bug in the builder itself.
1294 pub fn part_with(&mut self, name: &str, opts: &PartOptions) -> Result<Part, AuthorError> {
1295 let origin = self.placement(&Frame {
1296 origin: [0.0; 3],
1297 axis: [0.0, 0.0, 1.0],
1298 ref_dir: [1.0, 0.0, 0.0],
1299 })?;
1300 let a = &mut self.author;
1301 let product = a.add_product(
1302 opts.id.clone().unwrap_or_else(|| name.to_owned()),
1303 name.to_owned(),
1304 opts.description.clone(),
1305 vec![m::ProductContextRef::ProductContext(self.product_context)],
1306 )?;
1307 let formation = a.add_product_definition_formation(
1308 opts.version.clone().unwrap_or_default(),
1309 opts.version_description.clone(),
1310 m::ProductRef::Product(product),
1311 )?;
1312 let definition = a.add_product_definition(
1313 "design".to_owned(),
1314 opts.definition_description.clone(),
1315 m::ProductDefinitionFormationRef::ProductDefinitionFormation(formation),
1316 m::ProductDefinitionContextRef::ProductDefinitionContext(self.pd_context),
1317 )?;
1318 let shape = a.add_product_definition_shape(
1319 String::new(),
1320 None,
1321 m::CharacterizedDefinitionRef::ProductDefinition(definition),
1322 )?;
1323
1324 self.parts.push(PendingPart {
1325 product,
1326 formation,
1327 definition,
1328 shape,
1329 origin,
1330 items: Vec::new(),
1331 meshes: Vec::new(),
1332 });
1333 Ok(Part(self.parts.len() - 1))
1334 }
1335
1336 /// Attach a display mesh to `part`, optionally linked to the b-rep solid
1337 /// it tessellates (the link is what `MeshGroup::solid()` resolves on the
1338 /// read side). Triangle indices are 0-based into `points`; the builder
1339 /// converts to STEP's 1-based strip encoding. The mesh lands in its own
1340 /// `TESSELLATED_SHAPE_REPRESENTATION` on [`finish`](Self::finish).
1341 ///
1342 /// # Errors
1343 /// An id that did not come from this builder (another builder's, or out
1344 /// of range) fails validation with [`AuthorError`]; beyond that the
1345 /// wiring is fixed, so an error indicates a bug in the builder itself.
1346 pub fn mesh(
1347 &mut self,
1348 part: Part,
1349 name: &str,
1350 input: &MeshInput,
1351 of_solid: Option<SolidRef>,
1352 ) -> Result<m::TessellatedSolidId, AuthorError> {
1353 let a = &mut self.author;
1354 let npoints = i64::try_from(input.points.len()).unwrap_or(i64::MAX);
1355 let coordinates = a.add_coordinates_list(
1356 String::new(),
1357 npoints,
1358 input.points.iter().map(|p| p.to_vec()).collect(),
1359 )?;
1360 let normals: Vec<Vec<f64>> = match &input.normals {
1361 MeshNormalsInput::None => Vec::new(),
1362 MeshNormalsInput::Uniform(n) => vec![n.to_vec()],
1363 MeshNormalsInput::PerVertex(rows) => rows.iter().map(|n| n.to_vec()).collect(),
1364 };
1365 // One 3-index strip per triangle, 1-based. The read side's strip
1366 // decode swaps the last two indices of the first window, so encode
1367 // (p, q, r) as [p, r, q] to get the original winding back.
1368 let to_ix = |v: usize| i64::try_from(v + 1).unwrap_or(i64::MAX);
1369 let triangle_strips: Vec<Vec<i64>> = input
1370 .triangles
1371 .iter()
1372 .map(|&[p, q, r]| vec![to_ix(p), to_ix(r), to_ix(q)])
1373 .collect();
1374 let face = a.add_complex_triangulated_face(
1375 name.to_owned(),
1376 m::CoordinatesListRef::CoordinatesList(coordinates),
1377 npoints,
1378 normals,
1379 None,
1380 Vec::new(),
1381 triangle_strips,
1382 Vec::new(),
1383 )?;
1384 let solid = a.add_tessellated_solid(
1385 name.to_owned(),
1386 vec![m::TessellatedStructuredItemRef::ComplexTriangulatedFace(
1387 face,
1388 )],
1389 of_solid.map(|s| match s {
1390 SolidRef::Solid(id) => m::ManifoldSolidBrepRef::ManifoldSolidBrep(id),
1391 SolidRef::VoidSolid(id) => m::ManifoldSolidBrepRef::BrepWithVoids(id),
1392 }),
1393 )?;
1394 self.parts[part.0].meshes.push(solid);
1395 Ok(solid)
1396 }
1397
1398 /// A person at an organization — reusable across
1399 /// [`contributor`](Self::contributor) and approver roles.
1400 ///
1401 /// # Errors
1402 /// Propagates [`AuthorError`] from the strict constructors; the wiring
1403 /// here is fixed, so an error indicates a bug in the builder itself.
1404 pub fn person_and_org(
1405 &mut self,
1406 p: &PersonOrg,
1407 ) -> Result<m::PersonAndOrganizationId, AuthorError> {
1408 let a = &mut self.author;
1409 let person = a.add_person(
1410 p.person_id.clone(),
1411 p.last_name.clone(),
1412 p.first_name.clone(),
1413 None,
1414 None,
1415 None,
1416 )?;
1417 let organization = a.add_organization(None, p.organization.clone(), None)?;
1418 a.add_person_and_organization(
1419 m::PersonRef::Person(person),
1420 m::OrganizationRef::Organization(organization),
1421 )
1422 }
1423
1424 /// Record `who` as a contributor to the part's version under the given
1425 /// role (customary roles: `"creator"`, `"design_owner"`, `"checker"`).
1426 ///
1427 /// # Errors
1428 /// An id that did not come from this builder (another builder's, or out
1429 /// of range) fails validation with [`AuthorError`]; beyond that the
1430 /// wiring is fixed, so an error indicates a bug in the builder itself.
1431 pub fn contributor(
1432 &mut self,
1433 part: Part,
1434 role: &str,
1435 who: m::PersonAndOrganizationId,
1436 ) -> Result<(), AuthorError> {
1437 let a = &mut self.author;
1438 let role = a.add_person_and_organization_role(role.to_owned())?;
1439 a.add_applied_person_and_organization_assignment(
1440 m::PersonAndOrganizationRef::PersonAndOrganization(who),
1441 m::PersonAndOrganizationRoleRef::PersonAndOrganizationRole(role),
1442 vec![m::PersonAndOrganizationItemRef::ProductDefinitionFormation(
1443 self.parts[part.0].formation,
1444 )],
1445 )?;
1446 Ok(())
1447 }
1448
1449 /// Attach an approval to the part's version: a status word, a level,
1450 /// any approvers with their roles, and an optional approval date.
1451 ///
1452 /// # Errors
1453 /// An id that did not come from this builder (another builder's, or out
1454 /// of range) fails validation with [`AuthorError`]; beyond that the
1455 /// wiring is fixed, so an error indicates a bug in the builder itself.
1456 pub fn approve(
1457 &mut self,
1458 part: Part,
1459 input: &ApprovalInput,
1460 ) -> Result<m::ApprovalId, AuthorError> {
1461 let a = &mut self.author;
1462 let status = a.add_approval_status(input.status.clone())?;
1463 let approval = a.add_approval(
1464 m::ApprovalStatusRef::ApprovalStatus(status),
1465 input.level.clone(),
1466 )?;
1467 a.add_applied_approval_assignment(
1468 m::ApprovalRef::Approval(approval),
1469 vec![m::ApprovalItemRef::ProductDefinitionFormation(
1470 self.parts[part.0].formation,
1471 )],
1472 )?;
1473 for (who, role) in &input.approvers {
1474 let role = a.add_approval_role(role.clone())?;
1475 a.add_approval_person_organization(
1476 m::PersonOrganizationSelectRef::PersonAndOrganization(*who),
1477 m::ApprovalRef::Approval(approval),
1478 m::ApprovalRoleRef::ApprovalRole(role),
1479 )?;
1480 }
1481 if let Some(date) = input.date {
1482 let calendar = a.add_calendar_date(date.year, date.day, date.month)?;
1483 let zone = a.add_coordinated_universal_time_offset(0, None, m::AheadOrBehind::Exact)?;
1484 let time = a.add_local_time(
1485 date.hour,
1486 date.minute,
1487 None,
1488 m::CoordinatedUniversalTimeOffsetRef::CoordinatedUniversalTimeOffset(zone),
1489 )?;
1490 let date_time = a.add_date_and_time(
1491 m::DateRef::CalendarDate(calendar),
1492 m::LocalTimeRef::LocalTime(time),
1493 )?;
1494 a.add_approval_date_time(
1495 m::DateTimeSelectRef::DateAndTime(date_time),
1496 m::ApprovalRef::Approval(approval),
1497 )?;
1498 }
1499 Ok(approval)
1500 }
1501
1502 /// Attach an external document reference to the part's version.
1503 ///
1504 /// # Errors
1505 /// Propagates [`AuthorError`] from the strict constructors; the wiring
1506 /// here is fixed, so an error indicates a bug in the builder itself.
1507 pub fn document(&mut self, part: Part, d: &DocumentInput) -> Result<(), AuthorError> {
1508 let a = &mut self.author;
1509 let kind = a.add_document_type(d.kind.clone())?;
1510 let doc = a.add_document(
1511 d.id.clone(),
1512 d.name.clone(),
1513 d.description.clone(),
1514 m::DocumentTypeRef::DocumentType(kind),
1515 )?;
1516 a.add_applied_document_reference(
1517 m::DocumentRef::Document(doc),
1518 String::new(),
1519 vec![m::DocumentReferenceItemRef::ProductDefinitionFormation(
1520 self.parts[part.0].formation,
1521 )],
1522 )?;
1523 Ok(())
1524 }
1525
1526 /// Attach a security classification to the part's version.
1527 ///
1528 /// # Errors
1529 /// Propagates [`AuthorError`] from the strict constructors; the wiring
1530 /// here is fixed, so an error indicates a bug in the builder itself.
1531 pub fn security(
1532 &mut self,
1533 part: Part,
1534 name: &str,
1535 purpose: &str,
1536 level: &str,
1537 ) -> Result<(), AuthorError> {
1538 let a = &mut self.author;
1539 let level = a.add_security_classification_level(level.to_owned())?;
1540 let classification = a.add_security_classification(
1541 name.to_owned(),
1542 purpose.to_owned(),
1543 m::SecurityClassificationLevelRef::SecurityClassificationLevel(level),
1544 )?;
1545 a.add_applied_security_classification_assignment(
1546 m::SecurityClassificationRef::SecurityClassification(classification),
1547 vec![
1548 m::SecurityClassificationItemRef::ProductDefinitionFormation(
1549 self.parts[part.0].formation,
1550 ),
1551 ],
1552 )?;
1553 Ok(())
1554 }
1555
1556 /// Place `child` inside `parent` at the given frame — one assembly
1557 /// occurrence (the same child may be placed any number of times; each
1558 /// call is one instance). The usage occurrence is created immediately;
1559 /// its shape wiring is bound in [`finish`](Self::finish).
1560 ///
1561 /// # Errors
1562 /// Propagates [`AuthorError`] from the strict constructors; the wiring
1563 /// here is fixed, so an error indicates a bug in the builder itself.
1564 pub fn place(
1565 &mut self,
1566 parent: Part,
1567 child: Part,
1568 at: Frame,
1569 ) -> Result<m::NextAssemblyUsageOccurrenceId, AuthorError> {
1570 let nauo = self.author.add_next_assembly_usage_occurrence(
1571 format!("{}", self.placements.len() + 1),
1572 String::new(),
1573 None,
1574 m::ProductDefinitionOrReferenceRef::ProductDefinition(self.parts[parent.0].definition),
1575 m::ProductDefinitionOrReferenceRef::ProductDefinition(self.parts[child.0].definition),
1576 None,
1577 )?;
1578 let to = self.placement(&at)?;
1579 self.placements.push(PendingPlacement {
1580 nauo,
1581 parent: parent.0,
1582 child: child.0,
1583 to,
1584 });
1585 Ok(nauo)
1586 }
1587
1588 /// Materialize each part's shape representation (its origin placement
1589 /// plus the collected items) with its shape-definition link, add the
1590 /// customary product category, and emit the Part 21 text.
1591 ///
1592 /// # Errors
1593 /// Propagates [`AuthorError`] from the strict constructors; the wiring
1594 /// here is fixed, so an error indicates a bug in the builder itself.
1595 pub fn finish(mut self) -> Result<String, AuthorError> {
1596 let a = &mut self.author;
1597 // Pass 1: one shape representation per part (a part carrying a solid
1598 // is emitted as the customary ADVANCED_BREP_SHAPE_REPRESENTATION),
1599 // kept by part index for the placement wiring below.
1600 let mut part_reps: Vec<m::RepresentationOrRepresentationReferenceRef> =
1601 Vec::with_capacity(self.parts.len());
1602 for part in &self.parts {
1603 let mut items = vec![m::RepresentationItemRef::Axis2Placement3d(part.origin)];
1604 items.extend(part.items.iter().cloned());
1605 let has_solid = part.items.iter().any(|i| {
1606 matches!(
1607 i,
1608 m::RepresentationItemRef::ManifoldSolidBrep(_)
1609 | m::RepresentationItemRef::BrepWithVoids(_)
1610 )
1611 });
1612 let (rep, rep_or_ref) = if has_solid {
1613 let id = a.add_advanced_brep_shape_representation(
1614 String::new(),
1615 items,
1616 m::RepresentationContextRef::Complex(self.ctx),
1617 )?;
1618 (
1619 m::RepresentationRef::AdvancedBrepShapeRepresentation(id),
1620 m::RepresentationOrRepresentationReferenceRef::AdvancedBrepShapeRepresentation(
1621 id,
1622 ),
1623 )
1624 } else {
1625 let id = a.add_shape_representation(
1626 String::new(),
1627 items,
1628 m::RepresentationContextRef::Complex(self.ctx),
1629 )?;
1630 (
1631 m::RepresentationRef::ShapeRepresentation(id),
1632 m::RepresentationOrRepresentationReferenceRef::ShapeRepresentation(id),
1633 )
1634 };
1635 a.add_shape_definition_representation(
1636 m::RepresentedDefinitionRef::ProductDefinitionShape(part.shape),
1637 rep,
1638 )?;
1639 part_reps.push(rep_or_ref);
1640
1641 // A part with display meshes gets its own tessellated
1642 // representation alongside the shape one (multi-SDR per shape).
1643 if !part.meshes.is_empty() {
1644 let tsr = a.add_tessellated_shape_representation(
1645 String::new(),
1646 part.meshes
1647 .iter()
1648 .map(|&t| m::RepresentationItemRef::TessellatedSolid(t))
1649 .collect(),
1650 m::RepresentationContextRef::Complex(self.ctx),
1651 )?;
1652 a.add_shape_definition_representation(
1653 m::RepresentedDefinitionRef::ProductDefinitionShape(part.shape),
1654 m::RepresentationRef::TessellatedShapeRepresentation(tsr),
1655 )?;
1656 }
1657 }
1658 // Pass 2: assembly placements.
1659 bind_placements(a, &self.parts, &self.placements, &part_reps)?;
1660 if !self.parts.is_empty() {
1661 a.add_product_related_product_category(
1662 "part".to_owned(),
1663 None,
1664 self.parts
1665 .iter()
1666 .map(|p| m::ProductRef::Product(p.product))
1667 .collect(),
1668 )?;
1669 }
1670 // Hidden items (individual styled items and whole layers) collect
1671 // into one INVISIBILITY.
1672 if !self.hidden.is_empty() {
1673 a.add_invisibility(std::mem::take(&mut self.hidden))?;
1674 }
1675 // Styles are anchored in the customary presentation representation.
1676 if !self.styles.is_empty() {
1677 a.add_mechanical_design_geometric_presentation_representation(
1678 String::new(),
1679 self.styles
1680 .iter()
1681 .map(|&s| m::RepresentationItemRef::StyledItem(s))
1682 .collect(),
1683 m::RepresentationContextRef::Complex(self.ctx),
1684 )?;
1685 }
1686 let h = &self.header;
1687 let file_header = FileHeader {
1688 description: h.description.clone().unwrap_or_default(),
1689 file_name: h.file_name.clone().unwrap_or_default(),
1690 time_stamp: h.timestamp.clone().unwrap_or_else(now_iso_utc),
1691 authors: h.authors.clone(),
1692 organizations: h.organizations.clone(),
1693 preprocessor_version: concat!("step-io ", env!("CARGO_PKG_VERSION")).to_owned(),
1694 originating_system: h.originating_system.clone().unwrap_or_default(),
1695 authorisation: h.authorisation.clone().unwrap_or_default(),
1696 // The authoring layer stamps the AP242 schema identity in
1697 // `finish_with_header`; whatever is set here is overwritten.
1698 schema: crate::parser::SchemaId::default(),
1699 };
1700 Ok(self.author.finish_with_header(&file_header))
1701 }
1702}
1703
1704/// Assembly-placement wiring, deferred to `finish`: each occurrence gets its
1705/// placement shape (PDS over the NAUO) bound to an item-defined
1706/// transformation (item 1 = parent origin/from, item 2 = child placement/to)
1707/// through the customary relationship complex (`rep_1` = child, `rep_2` =
1708/// parent).
1709fn bind_placements(
1710 a: &mut Ap242Author,
1711 parts: &[PendingPart],
1712 placements: &[PendingPlacement],
1713 part_reps: &[m::RepresentationOrRepresentationReferenceRef],
1714) -> Result<(), AuthorError> {
1715 for placement in placements {
1716 let pds = a.add_product_definition_shape(
1717 "Placement".to_owned(),
1718 Some("Placement of an item".to_owned()),
1719 m::CharacterizedDefinitionRef::NextAssemblyUsageOccurrence(placement.nauo),
1720 )?;
1721 let idt = a.add_item_defined_transformation(
1722 String::new(),
1723 None,
1724 m::RepresentationItemRef::Axis2Placement3d(parts[placement.parent].origin),
1725 m::RepresentationItemRef::Axis2Placement3d(placement.to),
1726 )?;
1727 let rrwt = a.add_complex(vec![
1728 m::UnitPart::RepresentationRelationship {
1729 name: String::new(),
1730 description: None,
1731 rep_1: part_reps[placement.child].clone(),
1732 rep_2: part_reps[placement.parent].clone(),
1733 },
1734 m::UnitPart::RepresentationRelationshipWithTransformation {
1735 transformation_operator: m::TransformationRef::ItemDefinedTransformation(idt),
1736 },
1737 m::UnitPart::ShapeRepresentationRelationship,
1738 ])?;
1739 a.add_context_dependent_shape_representation(
1740 m::ShapeRepresentationRelationshipRef::Complex(rrwt),
1741 m::ProductDefinitionShapeRef::ProductDefinitionShape(pds),
1742 )?;
1743 }
1744 Ok(())
1745}
1746
1747#[cfg(test)]
1748mod tests {
1749 use super::iso_utc_from_unix;
1750
1751 #[test]
1752 fn iso_utc_from_unix_known_values() {
1753 assert_eq!(iso_utc_from_unix(0), "1970-01-01T00:00:00Z");
1754 // Leap-day boundary: 2024-02-29 23:59:59 UTC.
1755 assert_eq!(iso_utc_from_unix(1_709_251_199), "2024-02-29T23:59:59Z");
1756 // The day after: 2024-03-01 00:00:00 UTC.
1757 assert_eq!(iso_utc_from_unix(1_709_251_200), "2024-03-01T00:00:00Z");
1758 }
1759}