Skip to main content

brep_kernel/io/iges/
export.rs

1//! Kernel `BrepSolid` → IGES 5.3 Manifold Solid B-Rep Objects.
2//!
3//! Each solid becomes a **186** whose shells (**514**) hold faces (**510**)
4//! bounded by loops (**508**) over one edge list (**504**) and one vertex list
5//! (**502**), so neighbouring faces share their edges as they do in the
6//! kernel. A carrier the kernel recognises as analytic is written as the
7//! analytic entity, form 1 (**190** plane, **192** cylinder, **194** cone,
8//! **196** sphere, **198** torus, located by **116** points and **123**
9//! directions); any other carrier is a **128** B-spline. Edge curves are
10//! **126** B-splines, exact for the kernel's NURBS. A body's name goes in its
11//! directory-entry label and a **406** form 15 name property, and its colour,
12//! when given, in a **314** colour definition its directory entry points to.
13//!
14//! Every edge use on a plane or B-spline carrier also carries its parameter
15//! curve (a **126** in `(u, v, 0)`, running along the loop), so a reader
16//! never has to project an edge onto its face: on a plane it is the edge curve
17//! in the plane's own axes, exact; on a 128 it is the kernel's stored pcurve,
18//! whose parameter space the 128 shares; on a 192 cylinder, 196 sphere or 198
19//! torus it is the STEP writer's verified pcurve (`build_pcurve`) with its
20//! angles in degrees, the unit IGES readers take them in. A cone, and any use
21//! whose pcurve misses the export band, carries none and is projected by the
22//! reader.
23//!
24//! [`export_iges_assembly`] writes a document's product structure: each part
25//! once, its solids as **186**s inside a **308** subfigure definition, and each
26//! placement of it as a **408** instance moved by a **124**, named by its
27//! instance label, nested as deep as the structure goes.
28//!
29//! Orientation follows the importer's reading of the standard: a 508 runs
30//! counter-clockwise about its SURFACE's normal, and a face's orientation flag
31//! in its 514 says whether the face's outward normal agrees with that.
32
33use crate::curve::curve_to_plane_parameters;
34use crate::step::edge_subcurve;
35use crate::step::pcurve::{build_pcurve, EmittedCurve, EmittedFrame, EmittedSurface, Pcurve2d};
36use crate::topology::{BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord};
37use crate::{AnalyticSurface, KernelTolerances, NurbsCurve, NurbsSurface, Vec3, Vec4};
38use rustc_hash::FxHashMap as HashMap;
39
40use super::entities::{curve_to_126, surface_to_128};
41use super::writer::{
42    hollerith, real_field, GlobalParams, IgesEntity, IgesWriter, STATUS_DEFINITION, STATUS_DEPENDENT,
43    STATUS_INDEPENDENT,
44};
45use crate::{StepAssemblyExport, StepColors};
46
47/// One body to write: its name, the solid, and an optional sRGB colour.
48pub struct IgesBody<'a> {
49    pub name: String,
50    pub solid: &'a BrepSolid,
51    pub color: Option<[u8; 3]>,
52}
53
54/// Serialize solids as an IGES 5.3 document of Manifold Solid B-Rep Objects,
55/// every body under `name`.
56///
57/// `unit` is one of `millimeter|meter|centimeter|inch|foot` (default
58/// millimetre); coordinates are written verbatim, so callers pass the unit the
59/// kernel geometry is expressed in (millimetres in this kernel).
60pub fn export_iges(
61    solids: &[BrepSolid],
62    name: &str,
63    unit: &str,
64    timestamp: &str,
65) -> Result<String, String> {
66    let bodies: Vec<IgesBody<'_>> = solids
67        .iter()
68        .map(|solid| IgesBody { name: name.to_string(), solid, color: None })
69        .collect();
70    export_iges_bodies(&bodies, name, unit, timestamp)
71}
72
73/// [`export_iges`] with a name and colour per body.
74pub fn export_iges_bodies(
75    bodies: &[IgesBody<'_>],
76    name: &str,
77    unit: &str,
78    timestamp: &str,
79) -> Result<String, String> {
80    if bodies.is_empty() {
81        return Err("iges_export: no solids to export".into());
82    }
83    let mut writer = IgesWriter::new();
84    writer.add_start_line(&format!("IGES export of '{name}' from the BREP kernel."));
85    for body in bodies {
86        write_solid(&mut writer, body, STATUS_INDEPENDENT)?;
87    }
88    Ok(finish(writer, name, unit, timestamp))
89}
90
91/// A document's product structure as IGES: every product other than the root
92/// as a 308 subfigure definition holding its own solids (in its own frame) and
93/// the instances placed inside it, every occurrence as a 408 instance whose 124
94/// carries its placement and whose 406 name is its designator, and the root's
95/// own solids and instances standing on their own. Solids are named by their
96/// part-local scene names and coloured from `colors`, as the STEP writer does.
97pub fn export_iges_assembly(
98    assembly: &StepAssemblyExport,
99    colors: &StepColors,
100    name: &str,
101    unit: &str,
102    timestamp: &str,
103) -> Result<String, String> {
104    let root = assembly.root()?;
105    let mut writer = IgesWriter::new();
106    writer.add_start_line(&format!("IGES export of '{name}' from the BREP kernel."));
107    let mut subfigures: Vec<Option<(i64, usize)>> = vec![None; assembly.products.len()];
108    // A part's colours resolve through every occurrence namespace that reaches
109    // it, first in sorted order, as the STEP writer resolves them.
110    let prefixes = assembly.occurrence_prefixes(root);
111    let context = AssemblyContext { assembly, colors, root, prefixes: &prefixes };
112    write_product(&mut writer, &context, root, &mut subfigures)?;
113    Ok(finish(writer, name, unit, timestamp))
114}
115
116/// Write `index` (children first) and return nothing for the root, whose
117/// entities stand on their own, or its 308's pointer and nesting depth.
118struct AssemblyContext<'a> {
119    assembly: &'a StepAssemblyExport,
120    colors: &'a StepColors,
121    root: usize,
122    prefixes: &'a [Vec<String>],
123}
124
125fn write_product(
126    writer: &mut IgesWriter,
127    context: &AssemblyContext<'_>,
128    index: usize,
129    subfigures: &mut Vec<Option<(i64, usize)>>,
130) -> Result<(), String> {
131    let (assembly, root) = (context.assembly, context.root);
132    if subfigures[index].is_some() {
133        return Ok(());
134    }
135    let is_root = index == root;
136    let status = if is_root { STATUS_INDEPENDENT } else { STATUS_DEPENDENT };
137    let mut members = Vec::new();
138    let mut depth = 1;
139    let product = &assembly.products[index];
140    for (body_name, solid) in &product.bodies {
141        let color = context.colors.resolve(&context.prefixes[index], body_name);
142        let body = IgesBody { name: body_name.clone(), solid, color };
143        members.push(write_solid(writer, &body, status)?);
144    }
145    for occurrence in assembly.occurrences.iter().filter(|occurrence| occurrence.parent == index) {
146        write_product(writer, context, occurrence.child, subfigures)?;
147        let (definition, child_depth) =
148            subfigures[occurrence.child].ok_or("iges_export: an occurrence places the root product")?;
149        depth = depth.max(child_depth + 1);
150        let mut matrix = IgesEntity::new(
151            124,
152            // The top three rows, row-major: R11 R12 R13 T1 R21 ... T3.
153            occurrence.placement[..12].iter().map(|&value| real_field(value)).collect(),
154        );
155        matrix.status = STATUS_DEPENDENT;
156        let matrix = writer.add_entity(matrix);
157        let mut params = vec![definition.to_string(), real_field(0.0), real_field(0.0), real_field(0.0), real_field(1.0)];
158        if !occurrence.designator.is_empty() {
159            let mut label = IgesEntity::new(406, vec!["1".into(), hollerith(&occurrence.designator)]);
160            label.form = 15;
161            let property = writer.add_entity(label);
162            params.extend(["0".into(), "1".into(), property.to_string()]);
163        }
164        let mut instance = IgesEntity::new(408, params);
165        instance.transform = matrix;
166        instance.status = status;
167        instance.label = occurrence.designator.chars().filter(|c| !c.is_whitespace()).take(8).collect();
168        members.push(writer.add_entity(instance));
169    }
170    if is_root {
171        // The root is the file itself; mark it written so a cycle cannot recur.
172        subfigures[index] = Some((0, 0));
173        return Ok(());
174    }
175    let mut params = vec![depth.to_string(), hollerith(&product.name), members.len().to_string()];
176    params.extend(members.iter().map(i64::to_string));
177    let mut definition = IgesEntity::new(308, params);
178    definition.status = STATUS_DEFINITION;
179    definition.label = product.name.chars().filter(|c| !c.is_whitespace()).take(8).collect();
180    subfigures[index] = Some((writer.add_entity(definition), depth));
181    Ok(())
182}
183
184fn finish(writer: IgesWriter, name: &str, unit: &str, timestamp: &str) -> String {
185    let (units_flag, units_name) = units_for(unit);
186    let global = GlobalParams {
187        product_id: if name.is_empty() { "PART".into() } else { name.to_string() },
188        file_name: format!("{}.igs", if name.is_empty() { "part" } else { name }),
189        units_flag,
190        units_name,
191        timestamp: normalize_timestamp(timestamp),
192        min_resolution: 1e-7,
193    };
194    writer.finish(global)
195}
196
197/// The shared vertex and edge lists of one solid, and where each record went.
198struct Lists {
199    vertex_list: i64,
200    edge_list: i64,
201    vertex_index: HashMap<u64, usize>,
202    edge_index: HashMap<u64, usize>,
203    /// The band a quadric pcurve must verify to, as the STEP writer's.
204    band: f64,
205}
206
207/// Write one solid as a 186 with the given directory status; returns its
208/// pointer.
209pub(super) fn write_solid(writer: &mut IgesWriter, body: &IgesBody<'_>, status: &'static str) -> Result<i64, String> {
210    let solid = body.solid;
211    if solid.shells.is_empty() {
212        return Err(format!("iges_export: solid '{}' has no shells", body.name));
213    }
214    let shells = write_shells(writer, solid)?;
215    // The first shell bounds the solid; any others are its cavities.
216    let mut params = vec![shells[0].to_string(), "1".into(), (shells.len() - 1).to_string()];
217    for void in &shells[1..] {
218        params.push(void.to_string());
219        params.push("1".into());
220    }
221    // Name property, then the colour the directory entry will point to.
222    let label: String = body.name.chars().filter(|c| !c.is_whitespace()).take(8).collect();
223    let property = (!body.name.is_empty()).then(|| {
224        let mut name = IgesEntity::new(406, vec!["1".into(), hollerith(&body.name)]);
225        name.form = 15;
226        writer.add_entity(name)
227    });
228    let color = body.color.map(|[r, g, b]| {
229        let percent = |c: u8| real_field(f64::from(c) * 100.0 / 255.0);
230        writer.add_entity(IgesEntity::new(
231            314,
232            vec![percent(r), percent(g), percent(b), hollerith(&body.name)],
233        ))
234    });
235    if let Some(property) = property {
236        params.extend(["0".into(), "1".into(), property.to_string()]);
237    }
238    let mut entity = IgesEntity::new(186, params);
239    entity.label = label;
240    entity.color = color.map_or(0, |pointer| -pointer);
241    entity.status = status;
242    Ok(writer.add_entity(entity))
243}
244
245/// Each shell of `solid` as a closed 514 over the solid's shared edge and
246/// vertex lists, in shell order.
247pub(super) fn write_shells(writer: &mut IgesWriter, solid: &BrepSolid) -> Result<Vec<i64>, String> {
248    let lists = write_lists(writer, solid)?;
249    let mut shells = Vec::with_capacity(solid.shells.len());
250    for shell in &solid.shells {
251        let mut faces = Vec::with_capacity(shell.faces.len() * 2);
252        for face in &shell.faces {
253            let (pointer, agrees) = write_face(writer, solid, &lists, face)?;
254            faces.push(pointer.to_string());
255            faces.push(flag(agrees));
256        }
257        let mut params = vec![shell.faces.len().to_string()];
258        params.extend(faces);
259        let mut entity = IgesEntity::new(514, params);
260        entity.form = 1;
261        shells.push(writer.add_entity(entity));
262    }
263    Ok(shells)
264}
265
266fn flag(value: bool) -> String {
267    if value { "1" } else { "0" }.into()
268}
269
270fn write_lists(writer: &mut IgesWriter, solid: &BrepSolid) -> Result<Lists, String> {
271    let mut vertex_index = HashMap::default();
272    let mut vertex_params = vec![solid.vertices.len().to_string()];
273    for (index, vertex) in solid.vertices.iter().enumerate() {
274        vertex_index.insert(vertex.id, index + 1);
275        vertex_params.extend([real_field(vertex.point.x), real_field(vertex.point.y), real_field(vertex.point.z)]);
276    }
277    let mut vertex_entity = IgesEntity::new(502, vertex_params);
278    vertex_entity.form = 1;
279    let vertex_list = writer.add_entity(vertex_entity);
280
281    let mut edge_index = HashMap::default();
282    let edges: Vec<&EdgeRecord> = solid.edges.iter().filter(|edge| !edge.degenerate).collect();
283    let mut edge_params = vec![edges.len().to_string()];
284    for (index, edge) in edges.iter().enumerate() {
285        edge_index.insert(edge.id, index + 1);
286        let curve = writer.add_entity(curve_to_126(&edge_subcurve(edge)?)?);
287        let start = vertex_of(&vertex_index, edge.start_vertex_id)?;
288        let end = vertex_of(&vertex_index, edge.end_vertex_id)?;
289        edge_params.extend([
290            curve.to_string(),
291            vertex_list.to_string(),
292            start.to_string(),
293            vertex_list.to_string(),
294            end.to_string(),
295        ]);
296    }
297    let mut edge_entity = IgesEntity::new(504, edge_params);
298    edge_entity.form = 1;
299    let edge_list = writer.add_entity(edge_entity);
300    let band = KernelTolerances::for_solid(solid, 1e-7).export_knit;
301    Ok(Lists { vertex_list, edge_list, vertex_index, edge_index, band })
302}
303
304fn vertex_of(index: &HashMap<u64, usize>, id: u64) -> Result<usize, String> {
305    index.get(&id).copied().ok_or_else(|| format!("iges_export: dangling vertex id {id}"))
306}
307
308/// Write one face; returns its DE pointer and whether its outward normal
309/// agrees with its IGES surface's normal.
310fn write_face(
311    writer: &mut IgesWriter,
312    solid: &BrepSolid,
313    lists: &Lists,
314    face: &FaceRecord,
315) -> Result<(i64, bool), String> {
316    if face.loops.is_empty() {
317        return Err(format!("iges_export: face {} has no loops", face.id));
318    }
319    let (surface, reversed, carrier) = write_surface(writer, &face.surface)?;
320    let agrees = face.same_sense != reversed;
321    let mut loops = Vec::with_capacity(face.loops.len());
322    for loop_record in &face.loops {
323        loops.push(write_loop(writer, solid, lists, face, loop_record, agrees, &carrier)?);
324    }
325    // N loops, outer-loop flag (the kernel keeps its outer loop first).
326    let mut params = vec![surface.to_string(), face.loops.len().to_string(), "1".into()];
327    params.extend(loops.iter().map(i64::to_string));
328    let mut entity = IgesEntity::new(510, params);
329    entity.form = 1;
330    Ok((writer.add_entity(entity), agrees))
331}
332
333fn write_loop(
334    writer: &mut IgesWriter,
335    solid: &BrepSolid,
336    lists: &Lists,
337    face: &FaceRecord,
338    loop_record: &LoopRecord,
339    agrees: bool,
340    carrier: &Carrier,
341) -> Result<i64, String> {
342    // Kernel loops run counter-clockwise about the FACE's normal; a 508 runs
343    // counter-clockwise about its surface's, so a disagreeing face's loops
344    // are written backwards.
345    let mut uses: Vec<(&CoedgeRecord, bool)> = loop_record.coedges.iter().map(|c| (c, c.forward)).collect();
346    if !agrees {
347        uses.reverse();
348        for (_, forward) in &mut uses {
349            *forward = !*forward;
350        }
351    }
352    let mut entries = Vec::with_capacity(uses.len());
353    for (coedge, forward) in uses {
354        let edge_id = coedge.edge_id;
355        let edge = solid
356            .edges
357            .iter()
358            .find(|edge| edge.id == edge_id)
359            .ok_or_else(|| format!("iges_export: dangling edge id {edge_id}"))?;
360        let entry: Vec<String> = if edge.degenerate {
361            // A collapsed edge is a vertex use: TYPE 1 into the vertex list.
362            let vertex = vertex_of(&lists.vertex_index, edge.start_vertex_id)?;
363            // No parameter curve: measured on the corpus, OpenCASCADE closes a
364            // collapsed edge's (u, v) line itself and fails its own check on
365            // one we supply (m0007, m0139, m0143 invalid with it, valid without).
366            vec!["1".into(), lists.vertex_list.to_string(), vertex.to_string(), "1".into(), "0".into()]
367        } else {
368            let index = lists.edge_index.get(&edge_id).copied().ok_or("iges_export: edge missing from its list")?;
369            let mut entry = vec!["0".into(), lists.edge_list.to_string(), index.to_string(), flag(forward)];
370            match parameter_curve(carrier, &face.surface, lists.band, edge, coedge)? {
371                Some(curve) => {
372                    // A use's parameter curve runs along the loop, as OpenCASCADE
373                    // writes and reads it: reversed where the use runs against
374                    // its edge. The domain stays the edge curve's.
375                    let curve = if forward { curve } else { curve.reversed()? };
376                    let pointer = writer.add_entity(curve_to_126(&curve)?);
377                    // K = 1: one curve, not isoparametric.
378                    entry.extend(["1".into(), "0".into(), pointer.to_string()]);
379                }
380                None => entry.push("0".into()),
381            }
382            entry
383        };
384        entries.push(entry);
385    }
386    let mut params = vec![entries.len().to_string()];
387    for entry in entries {
388        params.extend(entry);
389    }
390    let mut entity = IgesEntity::new(508, params);
391    entity.form = 1;
392    Ok(writer.add_entity(entity))
393}
394
395/// What a face's carrier was written as, which decides its parameter curves.
396enum Carrier {
397    /// A 190: parameters are distances along these axes from the origin.
398    Plane { origin: Vec3, x_axis: Vec3, y_axis: Vec3 },
399    /// A 128 of the kernel surface itself: its parameters are the kernel's.
400    Spline,
401    /// A 192, 196 or 198, in the STEP entity's parameters with its angles in
402    /// degrees: u always, v too on a sphere or torus.
403    Quadric { surface: EmittedSurface, angular_v: bool },
404    /// A carrier written without parameter curves: a 194, whose v the readers
405    /// disagree on, or a torus a reader cannot tell u from v on.
406    Unparameterized,
407}
408
409/// Radians to the degrees IGES analytic surfaces are parameterized in.
410const DEGREES: f64 = 180.0 / std::f64::consts::PI;
411
412/// The parameter curve of one edge use, in the written carrier's parameters
413/// in the edge's direction over its 126's parameter range (`write_loop` turns
414/// it to the use's).
415fn parameter_curve(
416    carrier: &Carrier,
417    stored: &NurbsSurface,
418    band: f64,
419    edge: &EdgeRecord,
420    coedge: &CoedgeRecord,
421) -> Result<Option<NurbsCurve>, String> {
422    match carrier {
423        Carrier::Unparameterized => Ok(None),
424        Carrier::Quadric { surface, angular_v } => {
425            let curve = edge_subcurve(edge)?;
426            let [t0, t1] = curve.domain()?;
427            let oriented = if coedge.forward { coedge.pcurve.clone() } else { coedge.pcurve.reversed()? };
428            let outcome = build_pcurve(stored, surface, &EmittedCurve::Spline { curve }, &oriented, band)?;
429            let v_scale = if *angular_v { DEGREES } else { 1.0 };
430            // Scaling the homogeneous coordinates scales the curve exactly.
431            let scaled = |c: Vec4| Vec4 { x: c.x * DEGREES, y: c.y * v_scale, z: 0.0, w: c.w };
432            Ok(match outcome.curve {
433                Some(Pcurve2d::Line { point, vector }) => {
434                    let at = |s: f64| Vec4 { x: point[0] + s * vector[0], y: point[1] + s * vector[1], z: 0.0, w: 1.0 };
435                    Some(NurbsCurve::new(1, vec![t0, t0, t1, t1], vec![scaled(at(t0)), scaled(at(t1))])?)
436                }
437                Some(Pcurve2d::Spline(curve)) => {
438                    let points = curve.control_points.iter().map(|&c| scaled(c)).collect();
439                    Some(NurbsCurve::new(curve.degree, curve.knots.clone(), points)?)
440                }
441                // A 2D circle is only proposed against a 3D circle, which the
442                // IGES writer never emits; anything else goes without.
443                Some(Pcurve2d::Circle { .. }) | None => None,
444            })
445        }
446        // Projecting the edge curve's control points is affine, so the plane
447        // curve is exact and keeps the edge's knots and weights.
448        Carrier::Plane { origin, x_axis, y_axis } => {
449            Ok(Some(curve_to_plane_parameters(&edge_subcurve(edge)?, *origin, *x_axis, *y_axis)?))
450        }
451        Carrier::Spline => {
452            // The stored pcurve runs in loop direction, fraction-synchronised
453            // with the edge (as the STEP writer reads it).
454            let mut curve = if coedge.forward { coedge.pcurve.clone() } else { coedge.pcurve.reversed()? };
455            let [q0, q1] = curve.domain()?;
456            let [t0, t1] = edge_subcurve(edge)?.domain()?;
457            if q1 <= q0 || t1 <= t0 {
458                return Ok(None);
459            }
460            let scale = (t1 - t0) / (q1 - q0);
461            for knot in &mut curve.knots {
462                *knot = t0 + (*knot - q0) * scale;
463            }
464            Ok(Some(curve))
465        }
466    }
467}
468
469/// Write a carrier; returns its DE pointer, whether the written entity's
470/// normal is the reverse of the kernel surface's du x dv, and what it is.
471fn write_surface(writer: &mut IgesWriter, surface: &NurbsSurface) -> Result<(i64, bool, Carrier), String> {
472    if let Some(analytic) = surface.analytic() {
473        if let Some((pointer, reversed)) = write_analytic(writer, surface, analytic)? {
474            let emitted = |frame: &crate::analytic_surface::RevolutionFrame| EmittedFrame {
475                origin: frame.origin,
476                x_axis: frame.x_axis,
477                y_axis: frame.y_axis,
478                axis: frame.axis,
479                azimuth_sign: 1.0,
480            };
481            let carrier = match analytic {
482                AnalyticSurface::Plane { origin, u_dir, v_dir, .. } => {
483                    let normal = u_dir.cross(*v_dir).normalized()?;
484                    let x_axis = u_dir.normalized()?;
485                    Carrier::Plane { origin: *origin, x_axis, y_axis: normal.cross(x_axis) }
486                }
487                // `write_analytic` wrote a 192 exactly when the radius is constant.
488                AnalyticSurface::RuledRevolution { frame, rho0, rho1, .. }
489                    if (rho1 - rho0).abs() <= 1e-9 * (1.0 + rho0.abs().max(rho1.abs())) =>
490                {
491                    Carrier::Quadric {
492                        surface: EmittedSurface::Cylinder { frame: emitted(frame), radius: *rho0 },
493                        angular_v: false,
494                    }
495                }
496                AnalyticSurface::RuledRevolution { .. } => Carrier::Unparameterized,
497                AnalyticSurface::Sphere { frame, radius } => Carrier::Quadric {
498                    surface: EmittedSurface::Sphere { frame: emitted(frame), radius: *radius },
499                    angular_v: true,
500                },
501                // When the inner equator's radius R - r equals the tube's r, a
502                // parallel and a meridian are congruent circles, and OpenCASCADE
503                // reads a pcurve along one as along the other (m0291: R = 1,
504                // r = 0.5, an inner-equator edge taken 0.56 mm off). Such a torus
505                // goes without, and the reader projects.
506                AnalyticSurface::Torus { major_radius, minor_radius, .. }
507                    if ((major_radius - minor_radius) - minor_radius).abs() <= 1e-6 * major_radius.abs().max(1.0) =>
508                {
509                    Carrier::Unparameterized
510                }
511                AnalyticSurface::Torus { frame, major_radius, minor_radius } => Carrier::Quadric {
512                    surface: EmittedSurface::Torus {
513                        frame: emitted(frame),
514                        major_radius: *major_radius,
515                        minor_radius: *minor_radius,
516                    },
517                    angular_v: true,
518                },
519                AnalyticSurface::Revolution { .. } => Carrier::Unparameterized,
520            };
521            return Ok((pointer, reversed, carrier));
522        }
523    }
524    Ok((writer.add_entity(surface_to_128(surface)?), false, Carrier::Spline))
525}
526
527fn point_116(writer: &mut IgesWriter, p: Vec3) -> i64 {
528    writer.add_entity(IgesEntity::new(116, vec![real_field(p.x), real_field(p.y), real_field(p.z)]))
529}
530
531fn direction_123(writer: &mut IgesWriter, d: Vec3) -> i64 {
532    writer.add_entity(IgesEntity::new(123, vec![real_field(d.x), real_field(d.y), real_field(d.z)]))
533}
534
535/// The kernel surface's normal at the middle of its domain.
536fn sampled_normal(surface: &NurbsSurface) -> Result<(Vec3, Vec3), String> {
537    let [u0, u1] = surface.domain_u()?;
538    let [v0, v1] = surface.domain_v()?;
539    let (u, v) = (0.5 * (u0 + u1), 0.5 * (v0 + v1));
540    Ok((surface.evaluate(u, v)?, surface.normal(u, v)?))
541}
542
543fn write_analytic(
544    writer: &mut IgesWriter,
545    surface: &NurbsSurface,
546    analytic: &AnalyticSurface,
547) -> Result<Option<(i64, bool)>, String> {
548    let (point, normal) = sampled_normal(surface)?;
549    match analytic {
550        AnalyticSurface::Plane { origin, u_dir, v_dir, .. } => {
551            let (Ok(n), Ok(x)) = (u_dir.cross(*v_dir).normalized(), u_dir.normalized()) else {
552                return Ok(None);
553            };
554            // The plane is written with the kernel's own normal: no reversal.
555            let location = point_116(writer, *origin);
556            let normal = direction_123(writer, n);
557            let reference = direction_123(writer, x);
558            let mut entity = IgesEntity::new(190, vec![location.to_string(), normal.to_string(), reference.to_string()]);
559            entity.form = 1;
560            Ok(Some((writer.add_entity(entity), false)))
561        }
562        AnalyticSurface::RuledRevolution { frame, rho0, rho1, height } => {
563            let outward = {
564                let d = point.sub(frame.origin);
565                d.sub(frame.axis.scale(d.dot(frame.axis)))
566            };
567            let reversed = normal.dot(outward) < 0.0;
568            let radius_scale = 1.0 + rho0.abs().max(rho1.abs());
569            if (rho1 - rho0).abs() <= 1e-9 * radius_scale {
570                if *rho0 <= 0.0 {
571                    return Ok(None);
572                }
573                let location = point_116(writer, frame.origin);
574                let axis = direction_123(writer, frame.axis);
575                let reference = direction_123(writer, frame.x_axis);
576                let mut entity = IgesEntity::new(
577                    192,
578                    vec![location.to_string(), axis.to_string(), real_field(*rho0), reference.to_string()],
579                );
580                entity.form = 1;
581                return Ok(Some((writer.add_entity(entity), reversed)));
582            }
583            // As the STEP writer: a cone touching its apex keeps its NURBS, and
584            // the axis is turned so the radius grows along it.
585            let slope = (rho1 - rho0) / height;
586            let apex_margin = 2.0 * (1e-4 * height.abs().max(1.0) + 1e-9) * slope.abs();
587            if rho0.min(*rho1) <= apex_margin {
588                return Ok(None);
589            }
590            let direction = if slope >= 0.0 { frame.axis } else { frame.axis.scale(-1.0) };
591            let location = point_116(writer, frame.origin);
592            let axis = direction_123(writer, direction);
593            let reference = direction_123(writer, frame.x_axis);
594            let mut entity = IgesEntity::new(
595                194,
596                vec![
597                    location.to_string(),
598                    axis.to_string(),
599                    real_field(*rho0),
600                    real_field(slope.abs().atan().to_degrees()),
601                    reference.to_string(),
602                ],
603            );
604            entity.form = 1;
605            Ok(Some((writer.add_entity(entity), reversed)))
606        }
607        AnalyticSurface::Sphere { frame, radius } => {
608            let reversed = normal.dot(point.sub(frame.origin)) < 0.0;
609            let location = point_116(writer, frame.origin);
610            let axis = direction_123(writer, frame.axis);
611            let reference = direction_123(writer, frame.x_axis);
612            let mut entity = IgesEntity::new(
613                196,
614                vec![location.to_string(), real_field(*radius), axis.to_string(), reference.to_string()],
615            );
616            entity.form = 1;
617            Ok(Some((writer.add_entity(entity), reversed)))
618        }
619        AnalyticSurface::Torus { frame, major_radius, minor_radius } => {
620            let tube_centre = {
621                let d = point.sub(frame.origin);
622                let radial = d.sub(frame.axis.scale(d.dot(frame.axis)));
623                let Ok(direction) = radial.normalized() else {
624                    return Ok(None);
625                };
626                frame.origin.add(direction.scale(*major_radius))
627            };
628            let reversed = normal.dot(point.sub(tube_centre)) < 0.0;
629            let location = point_116(writer, frame.origin);
630            let axis = direction_123(writer, frame.axis);
631            let reference = direction_123(writer, frame.x_axis);
632            let mut entity = IgesEntity::new(
633                198,
634                vec![
635                    location.to_string(),
636                    axis.to_string(),
637                    real_field(*major_radius),
638                    real_field(*minor_radius),
639                    reference.to_string(),
640                ],
641            );
642            entity.form = 1;
643            Ok(Some((writer.add_entity(entity), reversed)))
644        }
645        AnalyticSurface::Revolution { .. } => Ok(None),
646    }
647}
648
649fn units_for(unit: &str) -> (i64, String) {
650    match unit.trim().to_ascii_lowercase().as_str() {
651        "inch" | "inches" | "in" => (1, "IN".into()),
652        "foot" | "feet" | "ft" => (4, "FT".into()),
653        "meter" | "metre" | "m" => (6, "M".into()),
654        "centimeter" | "centimetre" | "cm" => (10, "CM".into()),
655        _ => (2, "MM".into()),
656    }
657}
658
659fn normalize_timestamp(timestamp: &str) -> String {
660    if timestamp.trim().is_empty() {
661        "00000000.000000".into()
662    } else {
663        timestamp.to_string()
664    }
665}