Skip to main content

cadmpeg_codec_nx/
geometry.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Decode point and analytic geometry records from Parasolid neutral-binary data.
3//!
4//! The scanners recognize points; planes, cylinders, cones, spheres, and tori;
5//! and lines, circles, and ellipses. They validate record bounds, finite values,
6//! plausible scanner magnitudes, radii, and direction vectors before returning a
7//! carrier.
8//!
9//! Parasolid stores these fields as big-endian metre values. Returned coordinates
10//! and radii are in millimetres; unit vectors and curve parameters are unchanged.
11//! The scanners test supported field shifts caused by extended references and
12//! omit candidates that fail validation. Use [`crate::topology`] to resolve
13//! returned record offsets into topology.
14#![deny(clippy::disallowed_methods)]
15
16use cadmpeg_ir::be::{f64_at as read_f64, vec3_at as read_vec3};
17use cadmpeg_ir::geometry::{CurveGeometry, SurfaceGeometry};
18use cadmpeg_ir::math::{Point3, Vector3};
19
20/// Candidate byte shifts from expanded leading references. Envelope escapes are
21/// resolved by the fixed-record graph, where their independent shift is known.
22const SHIFTS: [usize; 6] = [0, 2, 4, 6, 8, 10];
23
24/// A decoded analytic surface and its source offset.
25#[derive(Debug, Clone)]
26pub struct DecodedSurface {
27    /// Byte offset of the record's type tag within the stream.
28    pub pos: usize,
29    /// The decoded surface geometry.
30    pub geometry: SurfaceGeometry,
31}
32
33/// A decoded analytic curve and its source offset.
34#[derive(Debug, Clone)]
35pub struct DecodedCurve {
36    /// Byte offset of the record's type tag within the stream.
37    pub pos: usize,
38    /// The decoded curve geometry.
39    pub geometry: CurveGeometry,
40}
41
42/// A decoded point and its source offset.
43#[derive(Debug, Clone)]
44pub struct DecodedPoint {
45    /// Byte offset of the record's `00 1d` tag within the stream.
46    pub pos: usize,
47    /// Position in millimetres.
48    pub position: Point3,
49}
50
51/// The analytic surface type tags and their fixed record lengths ([spec §4.1](https://github.com/cadmpeg/cadmpeg/blob/main/docs/formats/siemens_nx.md#41-fixed-record-families)).
52const SURFACE_TAGS: [(u8, usize); 5] = [
53    (0x32, 91),  // plane
54    (0x33, 99),  // cylinder
55    (0x34, 115), // cone
56    (0x35, 99),  // sphere
57    (0x36, 107), // torus
58];
59
60/// The analytic curve type tags and their fixed record lengths ([spec §4.1](https://github.com/cadmpeg/cadmpeg/blob/main/docs/formats/siemens_nx.md#41-fixed-record-families)).
61const CURVE_TAGS: [(u8, usize); 3] = [
62    (0x1e, 67),  // line
63    (0x1f, 99),  // circle
64    (0x20, 107), // ellipse
65];
66
67enum AnalyticRecord {
68    Point(DecodedPoint),
69    Surface(DecodedSurface),
70    Curve(DecodedCurve),
71}
72
73#[derive(Clone, Copy)]
74enum DecodeContext {
75    Scanner,
76    Graph,
77}
78
79/// Decode validated point records in source order.
80///
81/// Positions are returned in millimetres. Malformed and out-of-range candidates
82/// are skipped.
83pub fn points(stream: &[u8]) -> Vec<DecodedPoint> {
84    analytic_records(stream)
85        .into_iter()
86        .filter_map(|record| match record {
87            AnalyticRecord::Point(point) => Some(point),
88            AnalyticRecord::Surface(_) | AnalyticRecord::Curve(_) => None,
89        })
90        .collect()
91}
92
93/// Decode validated analytic surface records in source order.
94pub fn surfaces(stream: &[u8]) -> Vec<DecodedSurface> {
95    analytic_records(stream)
96        .into_iter()
97        .filter_map(|record| match record {
98            AnalyticRecord::Surface(surface) => Some(surface),
99            AnalyticRecord::Point(_) | AnalyticRecord::Curve(_) => None,
100        })
101        .collect()
102}
103
104/// Decode validated analytic curve records in source order.
105pub fn curves(stream: &[u8]) -> Vec<DecodedCurve> {
106    analytic_records(stream)
107        .into_iter()
108        .filter_map(|record| match record {
109            AnalyticRecord::Curve(curve) => Some(curve),
110            AnalyticRecord::Point(_) | AnalyticRecord::Surface(_) => None,
111        })
112        .collect()
113}
114
115fn analytic_records(stream: &[u8]) -> Vec<AnalyticRecord> {
116    let mut out = Vec::new();
117    let mut p = 0usize;
118    while p + 2 <= stream.len() {
119        if stream[p] != 0x00 {
120            p += 1;
121            continue;
122        }
123        let kind = stream[p + 1];
124        let candidate = if kind == 0x1d {
125            decode_point(stream, p)
126                .map(|(point, shift)| (AnalyticRecord::Point(point), p + 40 + shift))
127        } else if let Some((_, len)) = SURFACE_TAGS.iter().find(|(tag, _)| *tag == kind) {
128            decode_surface(stream, p, kind).map(|(geometry, shift)| {
129                (
130                    AnalyticRecord::Surface(DecodedSurface { pos: p, geometry }),
131                    p + *len + shift,
132                )
133            })
134        } else if let Some((_, len)) = CURVE_TAGS.iter().find(|(tag, _)| *tag == kind) {
135            decode_curve(stream, p, kind).map(|(geometry, shift)| {
136                (
137                    AnalyticRecord::Curve(DecodedCurve { pos: p, geometry }),
138                    p + *len + shift,
139                )
140            })
141        } else {
142            None
143        };
144        if let Some((record, end)) = candidate {
145            out.push(record);
146            p = end;
147        } else {
148            p += 1;
149        }
150    }
151    out
152}
153
154fn decode_point(stream: &[u8], p: usize) -> Option<(DecodedPoint, usize)> {
155    SHIFTS.into_iter().find_map(|shift| {
156        let xyz = read_vec3(stream, p + 16 + shift)?;
157        (xyz.iter().all(|value| {
158            value.is_finite() && value.abs() < 1.0e3 && (*value == 0.0 || value.abs() >= 1.0e-100)
159        }) && xyz.iter().any(|value| *value != 0.0))
160        .then_some((
161            DecodedPoint {
162                pos: p,
163                position: mm_point(xyz),
164            },
165            shift,
166        ))
167    })
168}
169
170/// Decode an analytic surface at tag position `p`, trying each candidate shift and
171/// returning the first whose payload passes the kind's validation gate.
172fn decode_surface(stream: &[u8], p: usize, kind: u8) -> Option<(SurfaceGeometry, usize)> {
173    for sh in SHIFTS {
174        let b = p + sh;
175        let geom = match kind {
176            0x32 => plane(stream, b, DecodeContext::Scanner),
177            0x33 => cylinder(stream, b, DecodeContext::Scanner),
178            0x34 => cone(stream, b, DecodeContext::Scanner),
179            0x35 => sphere(stream, b, DecodeContext::Scanner),
180            0x36 => torus(stream, b, DecodeContext::Scanner),
181            _ => None,
182        };
183        if let Some(geometry) = geom {
184            return Some((geometry, sh));
185        }
186    }
187    None
188}
189
190/// Decode an analytic curve at tag position `p`, trying each candidate shift.
191fn decode_curve(stream: &[u8], p: usize, kind: u8) -> Option<(CurveGeometry, usize)> {
192    for sh in SHIFTS {
193        let b = p + sh;
194        let geom = match kind {
195            0x1e => line(stream, b, DecodeContext::Scanner),
196            0x1f => circle(stream, b, DecodeContext::Scanner),
197            0x20 => ellipse(stream, b, DecodeContext::Scanner),
198            _ => None,
199        };
200        if let Some(geometry) = geom {
201            return Some((geometry, sh));
202        }
203    }
204    None
205}
206
207/// Decode a graph-owned analytic surface at its resolved logical-field shift.
208pub(crate) fn decode_surface_record(
209    record: &[u8],
210    kind: u8,
211    shift: usize,
212) -> Option<SurfaceGeometry> {
213    let b = shift;
214    match kind {
215        0x32 => plane(record, b, DecodeContext::Graph),
216        0x33 => cylinder(record, b, DecodeContext::Graph),
217        0x34 => cone(record, b, DecodeContext::Graph),
218        0x35 => sphere(record, b, DecodeContext::Graph),
219        0x36 => torus(record, b, DecodeContext::Graph),
220        _ => None,
221    }
222}
223
224/// Decode a graph-owned analytic curve at its resolved logical-field shift.
225pub(crate) fn decode_curve_record(record: &[u8], kind: u8, shift: usize) -> Option<CurveGeometry> {
226    let b = shift;
227    match kind {
228        0x1e => line(record, b, DecodeContext::Graph),
229        0x1f => circle(record, b, DecodeContext::Graph),
230        0x20 => ellipse(record, b, DecodeContext::Graph),
231        _ => None,
232    }
233}
234
235// --- Surface decoders (offsets from the common header, [§5.1](https://github.com/cadmpeg/cadmpeg/blob/main/docs/formats/siemens_nx.md#51-ownership-graph) / [§6.1](https://github.com/cadmpeg/cadmpeg/blob/main/docs/formats/siemens_nx.md#61-analytic-curves-and-surfaces)) ---
236
237fn plane(s: &[u8], b: usize, context: DecodeContext) -> Option<SurfaceGeometry> {
238    let origin = read_vec3(s, b + 19)?;
239    let normal = read_vec3(s, b + 43)?;
240    let x_axis = read_vec3(s, b + 67)?;
241    if !is_orthonormal_frame(normal, x_axis) || !valid_position(origin, context) {
242        return None;
243    }
244    Some(SurfaceGeometry::Plane {
245        origin: mm_point(origin),
246        normal: vec3(normal),
247        u_axis: vec3(x_axis),
248    })
249}
250
251fn cylinder(s: &[u8], b: usize, context: DecodeContext) -> Option<SurfaceGeometry> {
252    let origin = read_vec3(s, b + 19)?;
253    let axis = read_vec3(s, b + 43)?;
254    let radius = read_f64(s, b + 67)?;
255    let x_axis = read_vec3(s, b + 75)?;
256    if !is_orthonormal_frame(axis, x_axis)
257        || !valid_position(origin, context)
258        || !valid_radius(radius, context)
259    {
260        return None;
261    }
262    Some(SurfaceGeometry::Cylinder {
263        origin: mm_point(origin),
264        axis: vec3(axis),
265        ref_direction: vec3(x_axis),
266        radius: radius * 1000.0,
267    })
268}
269
270fn cone(s: &[u8], b: usize, context: DecodeContext) -> Option<SurfaceGeometry> {
271    let origin = read_vec3(s, b + 19)?;
272    let axis = read_vec3(s, b + 43)?;
273    let radius = read_f64(s, b + 67)?;
274    let sin_half = read_f64(s, b + 75)?;
275    let cos_half = read_f64(s, b + 83)?;
276    let x_axis = read_vec3(s, b + 91)?;
277    if !is_orthonormal_frame(axis, x_axis)
278        || !valid_position(origin, context)
279        || !valid_cone_radius(radius, context)
280    {
281        return None;
282    }
283    // The cone's half-angle is carried as its sine/cosine; the identity gate
284    // rejects a coincidental offset that does not hold a real (sin, cos) pair.
285    if !sin_half.is_finite()
286        || !cos_half.is_finite()
287        || sin_half == 0.0
288        || cos_half == 0.0
289        || (sin_half * sin_half + cos_half * cos_half - 1.0).abs() > 1.0e-6
290    {
291        return None;
292    }
293    Some(SurfaceGeometry::Cone {
294        origin: mm_point(origin),
295        axis: vec3(axis),
296        ref_direction: vec3(x_axis),
297        radius: radius * 1000.0,
298        ratio: 1.0,
299        half_angle: sin_half.abs().atan2(cos_half.abs()),
300    })
301}
302
303fn sphere(s: &[u8], b: usize, context: DecodeContext) -> Option<SurfaceGeometry> {
304    let center = read_vec3(s, b + 19)?;
305    let radius = read_f64(s, b + 43)?;
306    let axis = read_vec3(s, b + 51)?;
307    let x_axis = read_vec3(s, b + 75)?;
308    if !is_orthonormal_frame(axis, x_axis)
309        || !valid_position(center, context)
310        || !valid_radius(radius, context)
311    {
312        return None;
313    }
314    Some(SurfaceGeometry::Sphere {
315        center: mm_point(center),
316        axis: vec3(axis),
317        ref_direction: vec3(x_axis),
318        radius: radius * 1000.0,
319    })
320}
321
322fn torus(s: &[u8], b: usize, context: DecodeContext) -> Option<SurfaceGeometry> {
323    let center = read_vec3(s, b + 19)?;
324    let axis = read_vec3(s, b + 43)?;
325    let major = read_f64(s, b + 67)?;
326    let minor = read_f64(s, b + 75)?;
327    let x_axis = read_vec3(s, b + 83)?;
328    // A horn torus (major == minor) is valid; both radii must be positive and
329    // finite. A zero major radius is degenerate and rejected.
330    if !is_orthonormal_frame(axis, x_axis)
331        || !valid_position(center, context)
332        || !valid_radius(major, context)
333        || !valid_radius(minor, context)
334    {
335        return None;
336    }
337    Some(SurfaceGeometry::Torus {
338        center: mm_point(center),
339        axis: vec3(axis),
340        ref_direction: vec3(x_axis),
341        major_radius: major * 1000.0,
342        minor_radius: minor * 1000.0,
343    })
344}
345
346// --- Curve decoders ---
347
348fn line(s: &[u8], b: usize, context: DecodeContext) -> Option<CurveGeometry> {
349    let origin = read_vec3(s, b + 19)?;
350    let direction = read_vec3(s, b + 43)?;
351    if !is_unit(direction) || !valid_position(origin, context) {
352        return None;
353    }
354    Some(CurveGeometry::Line {
355        origin: mm_point(origin),
356        direction: vec3(direction),
357    })
358}
359
360fn circle(s: &[u8], b: usize, context: DecodeContext) -> Option<CurveGeometry> {
361    let center = read_vec3(s, b + 19)?;
362    let normal = read_vec3(s, b + 43)?;
363    let x_axis = read_vec3(s, b + 67)?;
364    let radius = read_f64(s, b + 91)?;
365    if !is_orthonormal_frame(normal, x_axis)
366        || !valid_position(center, context)
367        || !valid_radius(radius, context)
368    {
369        return None;
370    }
371    Some(CurveGeometry::Circle {
372        center: mm_point(center),
373        axis: vec3(normal),
374        ref_direction: vec3(x_axis),
375        radius: radius * 1000.0,
376    })
377}
378
379fn ellipse(s: &[u8], b: usize, context: DecodeContext) -> Option<CurveGeometry> {
380    let center = read_vec3(s, b + 19)?;
381    let normal = read_vec3(s, b + 43)?;
382    let x_axis = read_vec3(s, b + 67)?;
383    let major = read_f64(s, b + 91)?;
384    let minor = read_f64(s, b + 99)?;
385    if !is_orthonormal_frame(normal, x_axis) || !valid_position(center, context) {
386        return None;
387    }
388    if !valid_radius(major, context) || !valid_radius(minor, context) || minor > major {
389        return None;
390    }
391    Some(CurveGeometry::Ellipse {
392        center: mm_point(center),
393        axis: vec3(normal),
394        major_direction: vec3(x_axis),
395        major_radius: major * 1000.0,
396        minor_radius: minor * 1000.0,
397    })
398}
399
400// --- Primitives and gates ---
401
402/// Return whether a finite vector has unit length within the decode tolerance.
403fn is_unit(v: [f64; 3]) -> bool {
404    if !v.iter().all(|c| c.is_finite()) {
405        return false;
406    }
407    let n2 = v[0] * v[0] + v[1] * v[1] + v[2] * v[2];
408    (n2 - 1.0).abs() < 1.0e-6
409}
410
411/// Return whether two finite vectors form the serialized analytic normal/x-axis frame.
412fn is_orthonormal_frame(axis: [f64; 3], x_axis: [f64; 3]) -> bool {
413    is_unit(axis)
414        && is_unit(x_axis)
415        && (axis[0] * x_axis[0] + axis[1] * x_axis[1] + axis[2] * x_axis[2]).abs() < 1.0e-6
416}
417
418fn valid_position(v: [f64; 3], context: DecodeContext) -> bool {
419    v.iter().all(|coordinate| {
420        coordinate.is_finite()
421            && (*coordinate * 1000.0).is_finite()
422            && (matches!(context, DecodeContext::Graph) || coordinate.abs() < 1.0e3)
423    })
424}
425
426fn valid_radius(radius: f64, context: DecodeContext) -> bool {
427    radius.is_finite()
428        && (radius * 1000.0).is_finite()
429        && radius > 0.0
430        && (matches!(context, DecodeContext::Graph) || (radius > 1.0e-9 && radius < 1.0e3))
431}
432
433fn valid_cone_radius(radius: f64, context: DecodeContext) -> bool {
434    radius.is_finite()
435        && (radius * 1000.0).is_finite()
436        && radius >= 0.0
437        && (matches!(context, DecodeContext::Graph) || radius <= 1.0e3)
438}
439
440fn mm_point(v: [f64; 3]) -> Point3 {
441    Point3::new(v[0] * 1000.0, v[1] * 1000.0, v[2] * 1000.0)
442}
443
444fn vec3(v: [f64; 3]) -> Vector3 {
445    Vector3::new(v[0], v[1], v[2])
446}