Skip to main content

geop_ops_step/import/
structure.rs

1//! Where a file's bodies are: the shape representations holding them, the
2//! units and uncertainty each is in, and — for an assembly — where its
3//! parts are placed, flattened into one placement per body.
4//!
5//! A product's shape is a representation of items — solids, sheets,
6//! placements — in a context that gives its units. Representations related
7//! without a transformation (a product's shape and the B-rep representation
8//! holding its solid, say) share their coordinates. An assembly places the
9//! shape of each component in its own through a relationship with an
10//! `ITEM_DEFINED_TRANSFORMATION`, tied to the `NEXT_ASSEMBLY_USAGE_OCCURRENCE`
11//! saying which product is placed in which; a `MAPPED_ITEM` places one
12//! representation within another the same way. Every body is found by
13//! walking down from the products nothing places, composing placements on
14//! the way.
15
16use std::collections::{BTreeMap, BTreeSet, HashMap};
17
18use geop_core_math::{
19    geop_error::{GeopError, GeopResult},
20    union_find::UnionFind,
21};
22
23use super::{
24    geometry::{Placement, Scope},
25    reader::{Args, Reader, unsupported},
26};
27use crate::part21::{Instance, Value};
28
29/// A body to import: the solid or sheet `#id`, read in `scope`, and what it
30/// is called — its own name, or else the product it is the shape of.
31#[derive(Clone, Debug)]
32pub struct Item {
33    pub id: u64,
34    pub scope: Scope,
35    pub label: String,
36}
37
38/// The uncertainty of a file that states none, in millimetres.
39const DEFAULT_UNCERTAINTY: f64 = 1e-6;
40
41/// How deeply placements may nest: deeper than any real assembly, and a
42/// bound on a file whose placements form a cycle.
43const MAX_DEPTH: usize = 64;
44
45/// The kinds of instance that are bodies.
46const BODY_TYPES: [&str; 3] = [
47    "MANIFOLD_SOLID_BREP",
48    "BREP_WITH_VOIDS",
49    "SHELL_BASED_SURFACE_MODEL",
50];
51
52/// A body type the importer does not read, and why.
53const REFUSED_BODY_TYPES: [(&str, &str); 1] = [(
54    "FACETED_BREP",
55    "a solid of flat facets bounded by polygons rather than edges",
56)];
57
58/// The representation record of `instance`: one of the types whose
59/// parameters are a name, a list of items and a context.
60fn representation(instance: &Instance) -> Option<&crate::part21::Record> {
61    let records: Vec<&crate::part21::Record> = match instance {
62        Instance::Simple(r) => vec![r],
63        Instance::Complex(rs) => rs.iter().collect(),
64    };
65    records.into_iter().find(|r| {
66        r.name.ends_with("REPRESENTATION")
67            && r.args.len() == 3
68            && matches!(r.args[1], Value::List(_))
69            && matches!(r.args[2], Value::Ref(_))
70    })
71}
72
73impl Reader<'_> {
74    /// The scope of a representation's context: its length and angle units
75    /// and its uncertainty, placed nowhere yet.
76    fn context_scope(&self, context: u64) -> GeopResult<Scope> {
77        let instance = self.instance(context)?;
78        let mut scope = Scope {
79            length: 1.0,
80            angle: 1.0,
81            uncertainty: f64::NAN,
82            place: Placement::IDENTITY,
83        };
84        if let Some(record) = instance.record("GLOBAL_UNIT_ASSIGNED_CONTEXT") {
85            let args = Args {
86                id: context,
87                record,
88            };
89            for unit in args.references(0)? {
90                let u = self.instance(unit)?;
91                if u.is("LENGTH_UNIT") {
92                    scope.length = self.length_unit(unit)?;
93                } else if u.is("PLANE_ANGLE_UNIT") {
94                    scope.angle = self.angle_unit(unit)?;
95                }
96            }
97        }
98        if let Some(record) = instance.record("GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT") {
99            let args = Args {
100                id: context,
101                record,
102            };
103            for uncertainty in args.references(0)? {
104                let u = self.args(uncertainty, "UNCERTAINTY_MEASURE_WITH_UNIT")?;
105                let value = u.real(0)?;
106                let unit = u.reference(1)?;
107                if self.instance(unit)?.is("LENGTH_UNIT") {
108                    let mm = value * self.length_unit(unit)?;
109                    if mm > 0.0 && (scope.uncertainty.is_nan() || scope.uncertainty > mm) {
110                        scope.uncertainty = mm;
111                    }
112                }
113            }
114        }
115        if scope.uncertainty.is_nan() || scope.uncertainty <= 0.0 {
116            scope.uncertainty = DEFAULT_UNCERTAINTY;
117        }
118        Ok(scope)
119    }
120
121    /// The factor `value` with a unit gives, as `(value, unit)`: a
122    /// `*_MEASURE_WITH_UNIT`, simple or complex.
123    fn measure(&self, id: u64) -> GeopResult<(f64, u64)> {
124        let instance = self.instance(id)?;
125        let records: Vec<&crate::part21::Record> = match instance {
126            Instance::Simple(r) => vec![r],
127            Instance::Complex(rs) => rs.iter().collect(),
128        };
129        let record = records
130            .into_iter()
131            .find(|r| r.name.ends_with("MEASURE_WITH_UNIT") && r.args.len() == 2)
132            .ok_or_else(|| {
133                unsupported(
134                    id,
135                    instance,
136                    "a conversion factor that is not a measure with a unit",
137                )
138            })?;
139        let args = Args { id, record };
140        Ok((args.real(0)?, args.reference(1)?))
141    }
142
143    /// Millimetres per unit of the length unit `#id`.
144    fn length_unit(&self, id: u64) -> GeopResult<f64> {
145        let instance = self.instance(id)?;
146        if let Some(record) = instance.record("SI_UNIT") {
147            let args = Args { id, record };
148            if args.enumeration(1)? != "METRE" {
149                return Err(unsupported(
150                    id,
151                    instance,
152                    "a length unit that is not a metre",
153                ));
154            }
155            return Ok(1000.0 * prefix(id, instance, &args)?);
156        }
157        if let Some(record) = instance.record("CONVERSION_BASED_UNIT") {
158            let (value, unit) = self.measure(Args { id, record }.reference(1)?)?;
159            return Ok(value * self.length_unit(unit)?);
160        }
161        Err(unsupported(
162            id,
163            instance,
164            "a length unit neither SI nor converted from one",
165        ))
166    }
167
168    /// Radians per unit of the angle unit `#id`.
169    fn angle_unit(&self, id: u64) -> GeopResult<f64> {
170        let instance = self.instance(id)?;
171        if let Some(record) = instance.record("SI_UNIT") {
172            let args = Args { id, record };
173            if args.enumeration(1)? != "RADIAN" {
174                return Err(unsupported(
175                    id,
176                    instance,
177                    "an angle unit that is not a radian",
178                ));
179            }
180            return prefix(id, instance, &args);
181        }
182        if let Some(record) = instance.record("CONVERSION_BASED_UNIT") {
183            let (value, unit) = self.measure(Args { id, record }.reference(1)?)?;
184            return Ok(value * self.angle_unit(unit)?);
185        }
186        Err(unsupported(
187            id,
188            instance,
189            "an angle unit neither SI nor converted from one",
190        ))
191    }
192
193    /// Every body of the file, each with where it is placed.
194    pub fn bodies(&self) -> GeopResult<Vec<Item>> {
195        let exchange = self.exchange;
196        // The representations, their items and the scope of each.
197        let mut reps: BTreeMap<u64, (Vec<u64>, Scope, String)> = BTreeMap::new();
198        for (&id, instance) in &exchange.instances {
199            let Some(record) = representation(instance) else {
200                continue;
201            };
202            let args = Args { id, record };
203            let items = args.references(1)?;
204            let scope = self.context_scope(args.reference(2)?)?;
205            reps.insert(id, (items, scope, args.string(0).unwrap_or("").to_string()));
206        }
207        let rep_ids: Vec<u64> = reps.keys().copied().collect();
208        let index: HashMap<u64, usize> =
209            rep_ids.iter().enumerate().map(|(i, &id)| (id, i)).collect();
210
211        // Representations related without a transformation share their
212        // coordinates: one group.
213        let mut groups = UnionFind::new(rep_ids.len());
214        // Placements of one group's representations in another's:
215        // (parent rep, child rep, parent item, child item).
216        let mut placed: Vec<(u64, u64, u64, u64)> = Vec::new();
217        for (&id, instance) in &exchange.instances {
218            let Some(record) = instance.record("REPRESENTATION_RELATIONSHIP").or_else(|| {
219                instance
220                    .record("SHAPE_REPRESENTATION_RELATIONSHIP")
221                    .filter(|r| r.args.len() >= 4)
222            }) else {
223                continue;
224            };
225            let args = Args { id, record };
226            let (Ok(rep_1), Ok(rep_2)) = (args.reference(2), args.reference(3)) else {
227                continue;
228            };
229            let (Some(&a), Some(&b)) = (index.get(&rep_1), index.get(&rep_2)) else {
230                continue;
231            };
232            match instance.record("REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION") {
233                None => groups.union(a, b),
234                Some(record) => {
235                    let transformation = Args { id, record }.reference(0)?;
236                    let t = self.instance(transformation)?;
237                    let Some(record) = t.record("ITEM_DEFINED_TRANSFORMATION") else {
238                        return Err(unsupported(
239                            transformation,
240                            t,
241                            "a placement of a part not given by two placements",
242                        ));
243                    };
244                    let t_args = Args {
245                        id: transformation,
246                        record,
247                    };
248                    // The component is the first representation, the
249                    // assembly the second, each placement in its own
250                    // (the recommended practice); the products related
251                    // decide below where a writer did otherwise.
252                    placed.push((rep_2, rep_1, t_args.reference(3)?, t_args.reference(2)?));
253                }
254            }
255        }
256        let group_of = |groups: &mut UnionFind, rep: u64| index.get(&rep).map(|&i| groups.find(i));
257
258        // Products and the representations of their shapes, by id: where
259        // several definitions share a group, the first names it.
260        let mut shape_of_definition: BTreeMap<u64, Vec<u64>> = BTreeMap::new();
261        let mut definition_name: HashMap<u64, String> = HashMap::new();
262        for id in exchange.all_of("SHAPE_DEFINITION_REPRESENTATION") {
263            let args = self.args(id, "SHAPE_DEFINITION_REPRESENTATION")?;
264            let (Ok(pds), Ok(rep)) = (args.reference(0), args.reference(1)) else {
265                continue;
266            };
267            let Ok(pds) = self.args(pds, "PRODUCT_DEFINITION_SHAPE") else {
268                continue;
269            };
270            let Ok(definition) = pds.reference(2) else {
271                continue;
272            };
273            shape_of_definition.entry(definition).or_default().push(rep);
274            if let Some(name) = self.product_name(definition) {
275                definition_name.insert(definition, name);
276            }
277        }
278        let mut children: BTreeSet<u64> = BTreeSet::new();
279        // The parent and child product definitions of each occurrence.
280        let mut occurrences: HashMap<u64, (u64, u64)> = HashMap::new();
281        for id in exchange.all_of("NEXT_ASSEMBLY_USAGE_OCCURRENCE") {
282            let args = self.args(id, "NEXT_ASSEMBLY_USAGE_OCCURRENCE")?;
283            let (parent, child) = (args.reference(3)?, args.reference(4)?);
284            children.insert(child);
285            occurrences.insert(id, (parent, child));
286        }
287        // Turn each placement round where the occurrence it belongs to says
288        // its first representation is the assembly's.
289        for id in exchange.all_of("CONTEXT_DEPENDENT_SHAPE_REPRESENTATION") {
290            let args = self.args(id, "CONTEXT_DEPENDENT_SHAPE_REPRESENTATION")?;
291            let (Ok(relation), Ok(pds)) = (args.reference(0), args.reference(1)) else {
292                continue;
293            };
294            let Ok(pds) = self.args(pds, "PRODUCT_DEFINITION_SHAPE") else {
295                continue;
296            };
297            let Some(&(_, child)) = pds.reference(2).ok().and_then(|o| occurrences.get(&o)) else {
298                continue;
299            };
300            let rel = self.instance(relation)?;
301            let Some(record) = rel.record("REPRESENTATION_RELATIONSHIP") else {
302                continue;
303            };
304            let rel_args = Args {
305                id: relation,
306                record,
307            };
308            let rep_1 = rel_args.reference(2)?;
309            let child_groups: BTreeSet<usize> = shape_of_definition
310                .get(&child)
311                .into_iter()
312                .flatten()
313                .filter_map(|&rep| group_of(&mut groups, rep))
314                .collect();
315            if child_groups.is_empty()
316                || group_of(&mut groups, rep_1).is_some_and(|g| child_groups.contains(&g))
317            {
318                continue;
319            }
320            // The first representation is the assembly's: swap.
321            for p in placed.iter_mut() {
322                if p.1 == rep_1 {
323                    *p = (p.1, p.0, p.3, p.2);
324                }
325            }
326        }
327
328        // Each group's items, scopes and placements of others.
329        let mut members: BTreeMap<usize, Vec<u64>> = BTreeMap::new();
330        for &rep in &rep_ids {
331            let g = group_of(&mut groups, rep).expect("a representation");
332            members.entry(g).or_default().push(rep);
333        }
334        let mut child_groups: BTreeSet<usize> = BTreeSet::new();
335        let mut edges: BTreeMap<usize, Vec<(u64, u64, u64, u64)>> = BTreeMap::new();
336        for &(parent, child, parent_item, child_item) in &placed {
337            let (Some(pg), Some(cg)) =
338                (group_of(&mut groups, parent), group_of(&mut groups, child))
339            else {
340                continue;
341            };
342            child_groups.insert(cg);
343            edges
344                .entry(pg)
345                .or_default()
346                .push((parent, child, parent_item, child_item));
347        }
348
349        // The roots: the shapes of products nothing places, or, in a file
350        // without products, every group nothing places.
351        let mut roots: Vec<(usize, String)> = Vec::new();
352        let mut seen = BTreeSet::new();
353        for (&definition, shapes) in &shape_of_definition {
354            if children.contains(&definition) {
355                continue;
356            }
357            for &rep in shapes {
358                if let Some(g) = group_of(&mut groups, rep)
359                    && !child_groups.contains(&g)
360                    && seen.insert(g)
361                {
362                    roots.push((
363                        g,
364                        definition_name
365                            .get(&definition)
366                            .cloned()
367                            .unwrap_or_default(),
368                    ));
369                }
370            }
371        }
372        for &g in members.keys() {
373            if !child_groups.contains(&g) && seen.insert(g) && shape_of_definition.is_empty() {
374                roots.push((g, String::new()));
375            }
376        }
377        roots.sort();
378
379        let mut items = Vec::new();
380        for (root, label) in roots {
381            self.collect(
382                &reps,
383                &members,
384                &edges,
385                &mut groups,
386                &index,
387                root,
388                Placement::IDENTITY,
389                &label,
390                0,
391                &mut items,
392            )?;
393        }
394        Ok(items)
395    }
396
397    /// The name of the product the product definition `#definition` is
398    /// of.
399    fn product_name(&self, definition: u64) -> Option<String> {
400        let formation = self
401            .args(definition, "PRODUCT_DEFINITION")
402            .ok()?
403            .reference(2)
404            .ok()?;
405        let instance = self.instance(formation).ok()?;
406        let record = match instance {
407            Instance::Simple(r) => r,
408            Instance::Complex(rs) => rs.first()?,
409        };
410        let product = Args {
411            id: formation,
412            record,
413        }
414        .reference(2)
415        .ok()?;
416        let product = self.args(product, "PRODUCT").ok()?;
417        let name = product
418            .string(1)
419            .ok()
420            .filter(|s| !s.is_empty())
421            .or_else(|| product.string(0).ok())?;
422        Some(name.to_string())
423    }
424
425    /// The bodies of the group `group` and of everything placed in it, at
426    /// `place`.
427    #[allow(clippy::too_many_arguments)]
428    fn collect(
429        &self,
430        reps: &BTreeMap<u64, (Vec<u64>, Scope, String)>,
431        members: &BTreeMap<usize, Vec<u64>>,
432        edges: &BTreeMap<usize, Vec<(u64, u64, u64, u64)>>,
433        groups: &mut UnionFind,
434        index: &HashMap<u64, usize>,
435        group: usize,
436        place: Placement,
437        label: &str,
438        depth: usize,
439        out: &mut Vec<Item>,
440    ) -> GeopResult<()> {
441        if depth > MAX_DEPTH {
442            return Err(GeopError::new(format!(
443                "the file places parts within parts more than {MAX_DEPTH} deep: its placements form a cycle"
444            )));
445        }
446        for &rep in members.get(&group).into_iter().flatten() {
447            let (items, scope, rep_name) = &reps[&rep];
448            let label = if label.is_empty() {
449                rep_name.as_str()
450            } else {
451                label
452            };
453            for &item in items {
454                let instance = self.instance(item)?;
455                if let Some(name) = BODY_TYPES.iter().find(|t| instance.is(t)) {
456                    let own = self
457                        .args(item, name)
458                        .ok()
459                        .and_then(|a| a.string(0).ok())
460                        .unwrap_or("");
461                    out.push(Item {
462                        id: item,
463                        scope: Scope { place, ..*scope },
464                        label: if own.is_empty() {
465                            label.to_string()
466                        } else {
467                            own.to_string()
468                        },
469                    });
470                } else if let Some((_, why)) =
471                    REFUSED_BODY_TYPES.iter().find(|(t, _)| instance.is(t))
472                {
473                    return Err(unsupported(item, instance, why));
474                } else if instance.is("MAPPED_ITEM") {
475                    let args = self.args(item, "MAPPED_ITEM")?;
476                    let map = self.args(args.reference(1)?, "REPRESENTATION_MAP")?;
477                    let mapped = map.reference(1)?;
478                    let Some((_, child_scope, _)) = reps.get(&mapped) else {
479                        continue;
480                    };
481                    let origin = self.frame(child_scope, map.reference(0)?)?;
482                    let target = self.frame(scope, args.reference(2)?)?;
483                    let motion = Placement::of(&origin)
484                        .inverse()
485                        .then(&Placement::of(&target))
486                        .then(&place);
487                    let Some(&i) = index.get(&mapped) else {
488                        continue;
489                    };
490                    let g = groups.find(i);
491                    self.collect(
492                        reps,
493                        members,
494                        edges,
495                        groups,
496                        index,
497                        g,
498                        motion,
499                        label,
500                        depth + 1,
501                        out,
502                    )?;
503                }
504            }
505        }
506        for &(parent, child, parent_item, child_item) in edges.get(&group).into_iter().flatten() {
507            let parent_scope = reps[&parent].1;
508            let child_scope = reps[&child].1;
509            let target = self.frame(&parent_scope, parent_item)?;
510            let origin = self.frame(&child_scope, child_item)?;
511            let motion = Placement::of(&origin)
512                .inverse()
513                .then(&Placement::of(&target))
514                .then(&place);
515            let g = groups.find(index[&child]);
516            let child_label = reps[&child].2.clone();
517            self.collect(
518                reps,
519                members,
520                edges,
521                groups,
522                index,
523                g,
524                motion,
525                &child_label,
526                depth + 1,
527                out,
528            )?;
529        }
530        Ok(())
531    }
532}
533
534/// The factor of an SI unit's prefix.
535fn prefix(id: u64, instance: &Instance, args: &Args) -> GeopResult<f64> {
536    if args.is_null(0) {
537        return Ok(1.0);
538    }
539    Ok(match args.enumeration(0)? {
540        "EXA" => 1e18,
541        "PETA" => 1e15,
542        "TERA" => 1e12,
543        "GIGA" => 1e9,
544        "MEGA" => 1e6,
545        "KILO" => 1e3,
546        "HECTO" => 1e2,
547        "DECA" => 1e1,
548        "DECI" => 1e-1,
549        "CENTI" => 1e-2,
550        "MILLI" => 1e-3,
551        "MICRO" => 1e-6,
552        "NANO" => 1e-9,
553        "PICO" => 1e-12,
554        "FEMTO" => 1e-15,
555        "ATTO" => 1e-18,
556        other => {
557            return Err(unsupported(
558                id,
559                instance,
560                &format!("the unit prefix {other}"),
561            ));
562        }
563    })
564}