1use std::collections::{BTreeMap, BTreeSet, HashMap};
5use std::io::Write;
6
7use cadmpeg_ir::appearance::{Appearance, AppearanceTarget};
8use cadmpeg_ir::geometry::{
9 Curve, CurveGeometry, Pcurve, ProceduralCurve, ProceduralCurveDefinition, ProceduralSurface,
10 ProceduralSurfaceDefinition, Surface, SurfaceGeometry,
11};
12use cadmpeg_ir::ids::{OccurrenceId, ProductDefinitionId};
13use cadmpeg_ir::pmi::{
14 DimensionKind, GeometricToleranceKind, PmiDefinition, PmiQuantity, PmiTarget,
15};
16use cadmpeg_ir::presentation::PresentationItem;
17use cadmpeg_ir::products::{AssemblyGraph, OccurrenceParent, PrototypeReference};
18use cadmpeg_ir::report::{ExportReport, LossNote};
19use cadmpeg_ir::topology::{
20 Body, BodyKind, Coedge, Edge, Face, Loop, LoopBoundaryRole, Point, Sense, Shell, Vertex,
21};
22use cadmpeg_ir::CadIr;
23use cadmpeg_ir::{FidelityResolution, WritePath};
24
25use crate::error::StepError;
26use crate::geometry;
27use crate::loss::StepLossCode;
28use crate::options::{StepSchema, StepUnsupportedPolicy, StepWriteOptions};
29use crate::writer::{real, refs, string, Emitter, Ref};
30
31const EPS_IDENTITY: f64 = 1e-12;
32
33pub fn write_step(
49 ir: &CadIr,
50 w: &mut (impl Write + ?Sized),
51 opts: &StepWriteOptions,
52) -> Result<ExportReport, StepError> {
53 let mut b = Builder::new(ir, opts.schema);
54 b.build();
55 let report = b.finish_report();
56 let lines = b.emitter.into_lines();
57
58 if opts.unsupported == StepUnsupportedPolicy::Reject && !report.losses.is_empty() {
59 return Err(StepError::Unsupported(
60 report
61 .losses
62 .iter()
63 .map(|loss| loss.message.as_str())
64 .collect::<Vec<_>>()
65 .join("; "),
66 ));
67 }
68
69 write_header(w, opts)?;
70 writeln!(w, "DATA;")?;
71 for line in &lines {
72 writeln!(w, "{line}")?;
73 }
74 writeln!(w, "ENDSEC;")?;
75 writeln!(w, "END-ISO-10303-21;")?;
76 Ok(report)
77}
78
79fn write_header(w: &mut (impl Write + ?Sized), opts: &StepWriteOptions) -> std::io::Result<()> {
80 let ts = if opts.timestamp.is_empty() {
81 "1970-01-01T00:00:00"
82 } else {
83 &opts.timestamp
84 };
85 writeln!(w, "ISO-10303-21;")?;
86 writeln!(w, "HEADER;")?;
87 writeln!(
88 w,
89 "FILE_DESCRIPTION(({}),'2;1');",
90 string("CAD model exported by cadmpeg")
91 )?;
92 writeln!(
93 w,
94 "FILE_NAME({},{},({}),({}),{},{},{});",
95 string(&opts.product_name),
96 string(ts),
97 string(&opts.author),
98 string(&opts.organization),
99 string("cadmpeg-step"),
100 string(&opts.originating_system),
101 string("")
102 )?;
103 writeln!(w, "FILE_SCHEMA(({}));", string(opts.schema.file_schema()))?;
104 writeln!(w, "ENDSEC;")?;
105 Ok(())
106}
107
108#[derive(Clone, Copy)]
109struct ColorSpec<'a> {
110 color: cadmpeg_ir::topology::Color,
111 appearance: Option<&'a Appearance>,
112 binding_id: Option<&'a str>,
113}
114
115struct LoopSegment {
116 coedge_id: String,
117 start_vertex: String,
118 end_vertex: String,
119}
120
121pub(crate) struct Builder<'a> {
122 ir: &'a CadIr,
123 schema: StepSchema,
124 emitter: Emitter,
125 losses: Vec<LossNote>,
126 notes: Vec<String>,
127
128 points: HashMap<&'a str, &'a Point>,
129 bodies: HashMap<&'a str, &'a Body>,
130 shells: HashMap<&'a str, &'a Shell>,
131 faces: HashMap<&'a str, &'a Face>,
132 loops: HashMap<&'a str, &'a Loop>,
133 vertices: HashMap<&'a str, &'a Vertex>,
134 edges: HashMap<&'a str, &'a Edge>,
135 coedges: HashMap<&'a str, &'a Coedge>,
136 surfaces: HashMap<&'a str, &'a Surface>,
137 curves: HashMap<&'a str, &'a Curve>,
138 pcurves: HashMap<&'a str, &'a Pcurve>,
139 procedural_surfaces: HashMap<&'a str, &'a ProceduralSurface>,
140 procedural_curves: HashMap<&'a str, &'a ProceduralCurve>,
141 edge_coedges: HashMap<&'a str, Vec<(&'a str, &'a str)>>,
142
143 surface_refs: HashMap<String, Ref>,
144 curve_refs: HashMap<String, Ref>,
145 edge_refs: HashMap<String, Ref>,
146 vertex_refs: HashMap<String, Ref>,
147 point_refs: HashMap<String, Ref>,
148 pcurve_context: Option<Ref>,
149 active_surfaces: BTreeSet<String>,
150 pub(crate) active_curves: BTreeSet<String>,
151 written_procedural_surfaces: BTreeSet<String>,
152 written_procedural_curves: BTreeSet<String>,
153
154 curveless_edges: BTreeSet<String>,
158
159 unknown_surface_faces: BTreeSet<String>,
163
164 topology_relation_losses: BTreeSet<String>,
166
167 face_step_refs: HashMap<String, Ref>,
168 body_step_refs: HashMap<String, Ref>,
170 default_product_definition_shape: Option<Ref>,
171 body_shape_refs: HashMap<String, Ref>,
172 pub(crate) body_item_refs: HashMap<String, Vec<Ref>>,
173 body_step_item_refs: HashMap<String, Vec<Ref>>,
174 product_step_refs: HashMap<String, Ref>,
175 occurrence_step_refs: HashMap<String, Ref>,
176 tessellation_step_refs: HashMap<String, Ref>,
177 pmi_step_refs: HashMap<String, Ref>,
178 written_appearance_bindings: BTreeSet<String>,
179 unstyled_colors: usize,
180 unwritten_geometry_carriers: BTreeSet<String>,
181 unwritten_pcurve_carriers: BTreeSet<String>,
182 missing_parent_products: BTreeSet<String>,
183 empty_regions: BTreeSet<String>,
184 empty_wire_regions: BTreeSet<String>,
185 missing_wire_shells: BTreeSet<(String, String)>,
186 hidden_bodies_without_items: BTreeSet<String>,
187 dangling_appearance_bindings: BTreeSet<(String, String)>,
188 colorless_appearance_bindings: BTreeSet<(String, String)>,
189 written_pmi: usize,
190 length_unit: Option<Ref>,
191 angle_unit: Option<Ref>,
192 ratio_unit: Option<Ref>,
193 geometry_emission_depth: usize,
194}
195
196impl<'a> Builder<'a> {
197 pub(crate) fn new(ir: &'a CadIr, schema: StepSchema) -> Self {
198 let loop_surfaces = ir
199 .model
200 .faces
201 .iter()
202 .flat_map(|face| {
203 face.loops
204 .iter()
205 .map(move |loop_id| (loop_id.as_str(), face.surface.as_str()))
206 })
207 .collect::<HashMap<_, _>>();
208 let coedge_surfaces: HashMap<&str, &str> = ir
209 .model
210 .loops
211 .iter()
212 .filter_map(|loop_| {
213 loop_surfaces
214 .get(loop_.id.as_str())
215 .map(|surface| (loop_, *surface))
216 })
217 .flat_map(|(loop_, surface)| {
218 loop_
219 .coedges
220 .iter()
221 .map(move |coedge| (coedge.as_str(), surface))
222 })
223 .collect();
224 let mut edge_coedges = HashMap::<&str, Vec<(&str, &str)>>::new();
225 for coedge in &ir.model.coedges {
226 let Some(surface) = coedge_surfaces.get(coedge.id.as_str()) else {
227 continue;
228 };
229 for pcurve in &coedge.pcurves {
230 edge_coedges
231 .entry(coedge.edge.as_str())
232 .or_default()
233 .push((pcurve.pcurve.as_str(), *surface));
234 }
235 }
236 Builder {
237 ir,
238 schema,
239 emitter: Emitter::new(),
240 losses: Vec::new(),
241 notes: Vec::new(),
242 points: ir.model.points.iter().map(|p| (p.id.as_str(), p)).collect(),
243 bodies: ir
244 .model
245 .bodies
246 .iter()
247 .map(|body| (body.id.as_str(), body))
248 .collect(),
249 shells: ir
250 .model
251 .shells
252 .iter()
253 .map(|shell| (shell.id.as_str(), shell))
254 .collect(),
255 faces: ir
256 .model
257 .faces
258 .iter()
259 .map(|face| (face.id.as_str(), face))
260 .collect(),
261 loops: ir
262 .model
263 .loops
264 .iter()
265 .map(|loop_| (loop_.id.as_str(), loop_))
266 .collect(),
267 vertices: ir
268 .model
269 .vertices
270 .iter()
271 .map(|v| (v.id.as_str(), v))
272 .collect(),
273 edges: ir.model.edges.iter().map(|e| (e.id.as_str(), e)).collect(),
274 coedges: ir
275 .model
276 .coedges
277 .iter()
278 .map(|c| (c.id.as_str(), c))
279 .collect(),
280 surfaces: ir
281 .model
282 .surfaces
283 .iter()
284 .map(|s| (s.id.as_str(), s))
285 .collect(),
286 curves: ir.model.curves.iter().map(|c| (c.id.as_str(), c)).collect(),
287 pcurves: ir
288 .model
289 .pcurves
290 .iter()
291 .map(|p| (p.id.as_str(), p))
292 .collect(),
293 procedural_surfaces: ir
294 .model
295 .procedural_surfaces
296 .iter()
297 .map(|surface| (surface.surface.as_str(), surface))
298 .collect(),
299 procedural_curves: ir
300 .model
301 .procedural_curves
302 .iter()
303 .map(|curve| (curve.curve.as_str(), curve))
304 .collect(),
305 edge_coedges,
306 surface_refs: HashMap::new(),
307 curve_refs: HashMap::new(),
308 edge_refs: HashMap::new(),
309 vertex_refs: HashMap::new(),
310 point_refs: HashMap::new(),
311 pcurve_context: None,
312 active_surfaces: BTreeSet::new(),
313 active_curves: BTreeSet::new(),
314 written_procedural_surfaces: BTreeSet::new(),
315 written_procedural_curves: BTreeSet::new(),
316 curveless_edges: BTreeSet::new(),
317 unknown_surface_faces: BTreeSet::new(),
318 topology_relation_losses: BTreeSet::new(),
319 face_step_refs: HashMap::new(),
320 body_step_refs: HashMap::new(),
321 default_product_definition_shape: None,
322 body_shape_refs: HashMap::new(),
323 body_item_refs: HashMap::new(),
324 body_step_item_refs: HashMap::new(),
325 product_step_refs: HashMap::new(),
326 occurrence_step_refs: HashMap::new(),
327 tessellation_step_refs: HashMap::new(),
328 pmi_step_refs: HashMap::new(),
329 written_appearance_bindings: BTreeSet::new(),
330 unstyled_colors: 0,
331 unwritten_geometry_carriers: BTreeSet::new(),
332 unwritten_pcurve_carriers: BTreeSet::new(),
333 missing_parent_products: BTreeSet::new(),
334 empty_regions: BTreeSet::new(),
335 empty_wire_regions: BTreeSet::new(),
336 missing_wire_shells: BTreeSet::new(),
337 hidden_bodies_without_items: BTreeSet::new(),
338 dangling_appearance_bindings: BTreeSet::new(),
339 colorless_appearance_bindings: BTreeSet::new(),
340 written_pmi: 0,
341 length_unit: None,
342 angle_unit: None,
343 ratio_unit: None,
344 geometry_emission_depth: 0,
345 }
346 }
347
348 fn loss(&mut self, code: StepLossCode, message: String) {
349 self.losses.push(code.note(message));
350 }
351
352 fn topology_relation_loss(&mut self, identity: String, code: StepLossCode, message: String) {
353 if self.topology_relation_losses.insert(identity) {
354 self.loss(code, message);
355 }
356 }
357
358 pub(crate) fn build(&mut self) {
359 let context = self.emit_context();
360
361 let shape_items = self.emit_shape_items(context);
362 let mut standalone_items = self.emit_standalone_geometry();
363 let has_standalone_geometry = !standalone_items.is_empty();
364 let mut emitted_items = shape_items;
365 emitted_items.extend(standalone_items.iter().copied());
366 if emitted_items.is_empty() && !self.ir.model.bodies.is_empty() {
367 self.loss(
368 StepLossCode::NoExportableSolids,
369 "no exportable solids: the IR document contains no body/region/shell \
370 geometry, so the STEP representation is empty"
371 .to_string(),
372 );
373 emitted_items.clear();
374 }
375 let mut items = emitted_items;
376 let origin = geometry::placement(
377 &mut self.emitter,
378 cadmpeg_ir::math::Point3::new(0.0, 0.0, 0.0),
379 cadmpeg_ir::math::Vector3::new(0.0, 0.0, 1.0),
380 cadmpeg_ir::math::Vector3::new(1.0, 0.0, 0.0),
381 );
382 items.push(origin);
383
384 if self.ir.model.product_definitions.is_empty() {
385 let product_def_shape = self.emit_product_structure();
386 self.default_product_definition_shape = Some(product_def_shape);
387 let representation_kind = if !has_standalone_geometry
388 && !self.ir.model.bodies.is_empty()
389 && self.ir.model.bodies.iter().all(|body| {
390 body.transform
391 .is_none_or(|transform| is_identity(&transform.rows))
392 })
393 && self
394 .ir
395 .model
396 .bodies
397 .iter()
398 .all(|body| body.kind == BodyKind::Solid)
399 {
400 "ADVANCED_BREP_SHAPE_REPRESENTATION"
401 } else {
402 "SHAPE_REPRESENTATION"
403 };
404 let representation = self.emitter.emit(
405 representation_kind,
406 &format!("'',{},{context}", refs(&items)),
407 );
408 self.emitter.emit(
409 "SHAPE_DEFINITION_REPRESENTATION",
410 &format!("{product_def_shape},{representation}"),
411 );
412 } else {
413 self.emit_product_graph(context);
414 if has_standalone_geometry {
415 standalone_items.push(origin);
416 self.emitter.emit(
417 "SHAPE_REPRESENTATION",
418 &format!(
419 "{},{},{context}",
420 string("standalone geometry"),
421 refs(&standalone_items)
422 ),
423 );
424 }
425 }
426
427 self.emit_visibility();
428 self.emit_tessellations(context);
429 self.emit_presentation(context);
430 self.emit_pmi(context);
431 self.emit_layers();
432 self.note_unrepresented();
433 }
434
435 fn emit_presentation(&mut self, context: Ref) {
436 let ir = self.ir;
437 let appearances: HashMap<&str, &Appearance> = ir
438 .model
439 .appearances
440 .iter()
441 .map(|appearance| (appearance.id.as_str(), appearance))
442 .collect();
443 let mut body_colors: HashMap<&str, ColorSpec<'_>> = HashMap::new();
444 let mut face_colors: HashMap<&str, ColorSpec<'_>> = HashMap::new();
445 let mut dangling_appearance_bindings = BTreeSet::new();
446 let mut colorless_appearance_bindings = BTreeSet::new();
447 for binding in &ir.model.appearance_bindings {
448 let Some(appearance) = appearances.get(binding.appearance.as_str()).copied() else {
449 dangling_appearance_bindings
450 .insert((binding.id.clone(), binding.appearance.0.clone()));
451 continue;
452 };
453 let Some(color) = appearance.base_color else {
454 colorless_appearance_bindings
455 .insert((binding.id.clone(), binding.appearance.0.clone()));
456 continue;
457 };
458 let spec = ColorSpec {
459 color,
460 appearance: Some(appearance),
461 binding_id: Some(&binding.id),
462 };
463 match &binding.target {
464 AppearanceTarget::Body(id) => {
465 body_colors.entry(id.as_str()).or_insert(spec);
466 }
467 AppearanceTarget::Face(id) => {
468 face_colors.entry(id.as_str()).or_insert(spec);
469 }
470 AppearanceTarget::Surface(_)
471 | AppearanceTarget::Curve(_)
472 | AppearanceTarget::Point(_)
473 | AppearanceTarget::Edge(_)
474 | AppearanceTarget::Vertex(_)
475 | AppearanceTarget::Tessellation(_)
476 | AppearanceTarget::Source { .. } => {}
477 }
478 }
479 self.dangling_appearance_bindings
480 .extend(dangling_appearance_bindings);
481 self.colorless_appearance_bindings
482 .extend(colorless_appearance_bindings);
483 for body in &ir.model.bodies {
484 if let Some(color) = body.color {
485 body_colors.entry(body.id.as_str()).or_insert(ColorSpec {
486 color,
487 appearance: None,
488 binding_id: None,
489 });
490 }
491 }
492 for face in &ir.model.faces {
493 if let Some(color) = face.color {
494 face_colors.entry(face.id.as_str()).or_insert(ColorSpec {
495 color,
496 appearance: None,
497 binding_id: None,
498 });
499 }
500 }
501
502 let mut face_body: HashMap<&str, &str> = HashMap::new();
503 for region in &ir.model.regions {
504 let body = region.body.0.as_str();
505 for shell_id in ®ion.shells {
506 let Some(shell) = self.shells.get(shell_id.as_str()).copied() else {
507 continue;
508 };
509 for face in &shell.faces {
510 face_body.insert(face.0.as_str(), body);
511 }
512 }
513 }
514
515 let mut style_refs: HashMap<String, Ref> = HashMap::new();
519 let mut styled = Vec::new();
520 let mut faces: Vec<(String, Ref)> = self
521 .face_step_refs
522 .iter()
523 .map(|(id, r)| (id.clone(), *r))
524 .collect();
525 faces.sort_by(|a, b| a.0.cmp(&b.0));
526 let mut styled_bodies: BTreeSet<&str> = BTreeSet::new();
527 for (face_id, face) in &faces {
528 let own = face_colors.get(face_id.as_str()).copied();
529 let body = face_body.get(face_id.as_str()).copied();
530 let inherited = body.and_then(|b| body_colors.get(b).copied());
531 let Some(spec) = own.or(inherited) else {
532 continue;
533 };
534 if own.is_none() {
537 if let Some(b) = body {
538 styled_bodies.insert(b);
539 }
540 }
541 if let Some(binding_id) = spec.binding_id {
542 self.written_appearance_bindings
543 .insert(binding_id.to_string());
544 }
545 let name = spec
546 .appearance
547 .and_then(|appearance| appearance.name.as_deref())
548 .unwrap_or("");
549 let style = self.surface_style(spec.color, name, &mut style_refs);
550 styled.push(
551 self.emitter
552 .emit("STYLED_ITEM", &format!("'color',({style}),{face}")),
553 );
554 }
555 let mut direct_unstyled = BTreeSet::new();
556 for binding in &ir.model.appearance_bindings {
557 if self.written_appearance_bindings.contains(&binding.id) {
558 continue;
559 }
560 let Some(appearance) = appearances.get(binding.appearance.as_str()).copied() else {
561 continue;
562 };
563 let Some(color) = appearance.base_color else {
564 continue;
565 };
566 let (target, style_kind) = match &binding.target {
567 AppearanceTarget::Face(id) => {
568 (self.face_step_refs.get(id.as_str()).copied(), "surface")
569 }
570 AppearanceTarget::Surface(id) => {
571 (self.surface_refs.get(id.as_str()).copied(), "surface")
572 }
573 AppearanceTarget::Curve(id) => (self.curve_refs.get(id.as_str()).copied(), "curve"),
574 AppearanceTarget::Edge(id) => (self.edge_refs.get(id.as_str()).copied(), "curve"),
575 AppearanceTarget::Point(id) => (self.point_refs.get(id.as_str()).copied(), "point"),
576 AppearanceTarget::Vertex(id) => {
577 (self.vertex_refs.get(id.as_str()).copied(), "point")
578 }
579 AppearanceTarget::Tessellation(id) => {
580 (self.tessellation_step_refs.get(id).copied(), "surface")
581 }
582 AppearanceTarget::Body(_) | AppearanceTarget::Source { .. } => continue,
583 };
584 let Some(target) = target else {
585 let target_id = match &binding.target {
586 AppearanceTarget::Face(id) => id.0.clone(),
587 AppearanceTarget::Surface(id) => id.0.clone(),
588 AppearanceTarget::Curve(id) => id.0.clone(),
589 AppearanceTarget::Edge(id) => id.0.clone(),
590 AppearanceTarget::Point(id) => id.0.clone(),
591 AppearanceTarget::Vertex(id) => id.0.clone(),
592 AppearanceTarget::Tessellation(id) => id.clone(),
593 AppearanceTarget::Body(_) | AppearanceTarget::Source { .. } => continue,
594 };
595 direct_unstyled.insert(target_id);
596 continue;
597 };
598 let name = appearance.name.as_deref().unwrap_or("");
599 let style = match style_kind {
600 "surface" => self.surface_style(color, name, &mut style_refs),
601 "curve" => self.curve_style(color, name, &mut style_refs),
602 "point" => self.point_style(color, name, &mut style_refs),
603 _ => unreachable!(),
604 };
605 self.written_appearance_bindings.insert(binding.id.clone());
606 styled.push(
607 self.emitter
608 .emit("STYLED_ITEM", &format!("'color',({style}),{target}")),
609 );
610 }
611 for (body_id, spec) in &body_colors {
612 if styled_bodies.contains(body_id) {
613 continue;
614 }
615 let mut targets = self
616 .body_item_refs
617 .get(*body_id)
618 .cloned()
619 .unwrap_or_default();
620 if targets.is_empty() {
621 targets.extend(self.body_shape_refs.get(*body_id).copied());
622 }
623 if targets.is_empty() {
624 continue;
625 }
626 if let Some(binding_id) = spec.binding_id {
627 self.written_appearance_bindings
628 .insert(binding_id.to_string());
629 }
630 let name = spec
631 .appearance
632 .and_then(|appearance| appearance.name.as_deref())
633 .unwrap_or("");
634 let style = if self
635 .bodies
636 .get(*body_id)
637 .is_some_and(|body| body.kind == BodyKind::Wire)
638 {
639 self.curve_style(spec.color, name, &mut style_refs)
640 } else {
641 self.surface_style(spec.color, name, &mut style_refs)
642 };
643 styled.extend(targets.into_iter().map(|target| {
644 self.emitter
645 .emit("STYLED_ITEM", &format!("'color',({style}),{target}"))
646 }));
647 styled_bodies.insert(body_id);
648 }
649 let emitted: BTreeSet<&str> = self.face_step_refs.keys().map(String::as_str).collect();
654 let mut unstyled_targets = face_colors
655 .keys()
656 .filter(|id| !emitted.contains(**id as &str))
657 .map(|id| (*id).to_string())
658 .collect::<BTreeSet<_>>();
659 unstyled_targets.extend(
660 body_colors
661 .keys()
662 .filter(|id| !styled_bodies.contains(**id as &str))
663 .map(|id| (*id).to_string()),
664 );
665 unstyled_targets.extend(direct_unstyled);
666 self.unstyled_colors = unstyled_targets.len();
667 if styled.is_empty() {
668 return;
669 }
670 self.emitter.emit(
671 "MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION",
672 &format!("'',{},{context}", refs(&styled)),
673 );
674 }
675
676 fn surface_style(
677 &mut self,
678 color: cadmpeg_ir::topology::Color,
679 name: &str,
680 cache: &mut HashMap<String, Ref>,
681 ) -> Ref {
682 let rgb = format!(
683 "{},{},{}",
684 real(f64::from(color.r)),
685 real(f64::from(color.g)),
686 real(f64::from(color.b))
687 );
688 let key = format!("surface:{name}:{rgb}");
689 if let Some(style) = cache.get(&key) {
690 return *style;
691 }
692 let colour = self
693 .emitter
694 .emit("COLOUR_RGB", &format!("{},{rgb}", string(name)));
695 let fill_colour = self
696 .emitter
697 .emit("FILL_AREA_STYLE_COLOUR", &format!("'',{colour}"));
698 let fill = self
699 .emitter
700 .emit("FILL_AREA_STYLE", &format!("'',({fill_colour})"));
701 let style_fill = self
702 .emitter
703 .emit("SURFACE_STYLE_FILL_AREA", &fill.to_string());
704 let side = self
705 .emitter
706 .emit("SURFACE_SIDE_STYLE", &format!("'',({style_fill})"));
707 let usage = self
708 .emitter
709 .emit("SURFACE_STYLE_USAGE", &format!(".BOTH.,{side}"));
710 let assignment = self
711 .emitter
712 .emit("PRESENTATION_STYLE_ASSIGNMENT", &format!("({usage})"));
713 cache.insert(key, assignment);
714 assignment
715 }
716
717 fn curve_style(
718 &mut self,
719 color: cadmpeg_ir::topology::Color,
720 name: &str,
721 cache: &mut HashMap<String, Ref>,
722 ) -> Ref {
723 let rgb = format!(
724 "{},{},{}",
725 real(f64::from(color.r)),
726 real(f64::from(color.g)),
727 real(f64::from(color.b))
728 );
729 let key = format!("curve:{name}:{rgb}");
730 if let Some(style) = cache.get(&key) {
731 return *style;
732 }
733 let colour = self
734 .emitter
735 .emit("COLOUR_RGB", &format!("{},{rgb}", string(name)));
736 let font = self
737 .emitter
738 .emit("DRAUGHTING_PRE_DEFINED_CURVE_FONT", &string("continuous"));
739 let curve = self.emitter.emit(
740 "CURVE_STYLE",
741 &format!("'',{font},POSITIVE_LENGTH_MEASURE(0.1),{colour}"),
742 );
743 let assignment = self
744 .emitter
745 .emit("PRESENTATION_STYLE_ASSIGNMENT", &format!("({curve})"));
746 cache.insert(key, assignment);
747 assignment
748 }
749
750 fn point_style(
751 &mut self,
752 color: cadmpeg_ir::topology::Color,
753 name: &str,
754 cache: &mut HashMap<String, Ref>,
755 ) -> Ref {
756 let rgb = format!(
757 "{},{},{}",
758 real(f64::from(color.r)),
759 real(f64::from(color.g)),
760 real(f64::from(color.b))
761 );
762 let key = format!("point:{name}:{rgb}");
763 if let Some(style) = cache.get(&key) {
764 return *style;
765 }
766 let colour = self
767 .emitter
768 .emit("COLOUR_RGB", &format!("{},{rgb}", string(name)));
769 let point = self.emitter.emit(
770 "POINT_STYLE",
771 &format!("'',.DOT.,POSITIVE_LENGTH_MEASURE(1.),{colour}"),
772 );
773 let assignment = self
774 .emitter
775 .emit("PRESENTATION_STYLE_ASSIGNMENT", &format!("({point})"));
776 cache.insert(key, assignment);
777 assignment
778 }
779
780 fn emit_layers(&mut self) {
781 for layer in self.ir.model.presentation_layers.clone() {
782 let mut assigned = Vec::new();
783 let mut unsupported = 0usize;
784 for item in layer.items {
785 let references = match item {
786 PresentationItem::Body { body } => self
787 .body_item_refs
788 .get(body.as_str())
789 .cloned()
790 .filter(|references| !references.is_empty())
791 .or_else(|| {
792 self.body_shape_refs
793 .get(body.as_str())
794 .copied()
795 .map(|reference| vec![reference])
796 })
797 .unwrap_or_default(),
798 PresentationItem::Face { face } => self
799 .face_step_refs
800 .get(face.as_str())
801 .copied()
802 .into_iter()
803 .collect(),
804 PresentationItem::Edge { edge } => self
805 .edge_refs
806 .get(edge.as_str())
807 .copied()
808 .into_iter()
809 .collect(),
810 PresentationItem::Vertex { vertex } => self
811 .vertex_refs
812 .get(vertex.as_str())
813 .copied()
814 .into_iter()
815 .collect(),
816 PresentationItem::Curve { curve } => self
817 .curve_refs
818 .get(curve.as_str())
819 .copied()
820 .into_iter()
821 .collect(),
822 PresentationItem::Surface { surface } => self
823 .surface_refs
824 .get(surface.as_str())
825 .copied()
826 .into_iter()
827 .collect(),
828 PresentationItem::Product { product } => self
829 .product_step_refs
830 .get(product.as_str())
831 .copied()
832 .into_iter()
833 .collect(),
834 PresentationItem::Occurrence { occurrence } => self
835 .occurrence_step_refs
836 .get(occurrence.as_str())
837 .copied()
838 .into_iter()
839 .collect(),
840 PresentationItem::Pmi { annotation } => self
841 .pmi_step_refs
842 .get(annotation.as_str())
843 .copied()
844 .into_iter()
845 .collect(),
846 PresentationItem::Point { point } => self
847 .point_refs
848 .get(point.as_str())
849 .copied()
850 .into_iter()
851 .collect(),
852 PresentationItem::Source { .. } => Vec::new(),
853 PresentationItem::Tessellation { tessellation } => self
854 .tessellation_step_refs
855 .get(&tessellation)
856 .copied()
857 .into_iter()
858 .collect(),
859 };
860 if references.is_empty() {
861 unsupported += 1;
862 } else {
863 assigned.extend(references);
864 }
865 }
866 if unsupported > 0 {
867 self.loss(
868 StepLossCode::LayerItemWithoutCarrier,
869 format!(
870 "layer '{}' has {unsupported} item(s) without a writable STEP carrier",
871 layer.name
872 ),
873 );
874 }
875 if !assigned.is_empty() {
876 self.emitter.emit(
877 "PRESENTATION_LAYER_ASSIGNMENT",
878 &format!(
879 "{},{},{}",
880 string(&layer.name),
881 string(layer.description.as_deref().unwrap_or("")),
882 refs(&assigned)
883 ),
884 );
885 }
886 }
887 }
888
889 fn emit_product_structure(&mut self) -> Ref {
890 let name = self
891 .ir
892 .model
893 .bodies
894 .first()
895 .and_then(|b| b.name.clone())
896 .unwrap_or_else(|| "cadmpeg_model".to_string());
897
898 let (application, protocol, year) = self.schema.application_protocol();
899 let app_ctx = self
900 .emitter
901 .emit("APPLICATION_CONTEXT", &string(application));
902 self.emitter.emit(
903 "APPLICATION_PROTOCOL_DEFINITION",
904 &format!(
905 "{},{},{year},{app_ctx}",
906 string("international standard"),
907 string(protocol)
908 ),
909 );
910 let prod_ctx = self.emitter.emit(
911 "PRODUCT_CONTEXT",
912 &format!("'',{app_ctx},{}", string("mechanical")),
913 );
914 let product = self.emitter.emit(
915 "PRODUCT",
916 &format!("{},{},'',({prod_ctx})", string(&name), string(&name)),
917 );
918 let formation = self
919 .emitter
920 .emit("PRODUCT_DEFINITION_FORMATION", &format!("'','',{product}"));
921 let pd_ctx = self.emitter.emit(
922 "PRODUCT_DEFINITION_CONTEXT",
923 &format!(
924 "{},{app_ctx},{}",
925 string("part definition"),
926 string("design")
927 ),
928 );
929 let product_def = self.emitter.emit(
930 "PRODUCT_DEFINITION",
931 &format!("{},'',{formation},{pd_ctx}", string("design")),
932 );
933 self.emitter
934 .emit("PRODUCT_DEFINITION_SHAPE", &format!("'','',{product_def}"))
935 }
936
937 fn emit_product_graph(&mut self, context: Ref) {
938 let (application, protocol, year) = self.schema.application_protocol();
939 let app_context = self
940 .emitter
941 .emit("APPLICATION_CONTEXT", &string(application));
942 self.emitter.emit(
943 "APPLICATION_PROTOCOL_DEFINITION",
944 &format!(
945 "{},{},{year},{app_context}",
946 string("international standard"),
947 string(protocol)
948 ),
949 );
950 let product_context = self.emitter.emit(
951 "PRODUCT_CONTEXT",
952 &format!("'',{app_context},{}", string("mechanical")),
953 );
954 let definition_context = self.emitter.emit(
955 "PRODUCT_DEFINITION_CONTEXT",
956 &format!(
957 "{},{app_context},{}",
958 string("part definition"),
959 string("design")
960 ),
961 );
962
963 let ir = self.ir;
964 let products = &ir.model.product_definitions;
965 let occurrences = &ir.model.occurrences;
966 let Ok(graph) = AssemblyGraph::new(occurrences) else {
967 self.loss(
968 StepLossCode::AssemblyGraphInvalid,
969 "assembly occurrence graph is invalid".into(),
970 );
971 return;
972 };
973 let occurrence_products = occurrences
974 .iter()
975 .filter_map(|occurrence| match &occurrence.prototype {
976 PrototypeReference::Local { definition } => {
977 Some((occurrence.id.clone(), definition.clone()))
978 }
979 PrototypeReference::External { .. } | PrototypeReference::Unresolved => None,
980 })
981 .collect::<HashMap<OccurrenceId, ProductDefinitionId>>();
982 let mut product_origins = HashMap::<ProductDefinitionId, Ref>::new();
983 for product in products {
984 product_origins.insert(
985 product.id.clone(),
986 geometry::placement(
987 &mut self.emitter,
988 cadmpeg_ir::math::Point3::new(0.0, 0.0, 0.0),
989 cadmpeg_ir::math::Vector3::new(0.0, 0.0, 1.0),
990 cadmpeg_ir::math::Vector3::new(1.0, 0.0, 0.0),
991 ),
992 );
993 }
994 let mut representation_placements = HashMap::<ProductDefinitionId, Vec<Ref>>::new();
995 let mut occurrence_placements = HashMap::<OccurrenceId, (Ref, Ref)>::new();
996 for occurrence in occurrences {
997 let OccurrenceParent::Occurrence { occurrence: parent } = &occurrence.parent else {
998 continue;
999 };
1000 let Some(parent_product) = occurrence_products.get(parent) else {
1001 continue;
1002 };
1003 let Some(child_product) = occurrence_products.get(&occurrence.id) else {
1004 continue;
1005 };
1006 let Some(&from) = product_origins.get(child_product) else {
1007 continue;
1008 };
1009 let transform = if occurrence.link_transform.unwrap_or(false) {
1010 occurrence.transform.compose(occurrence.prototype_transform)
1011 } else {
1012 occurrence.transform
1013 };
1014 if !transform.is_proper_rigid() || occurrence.scale != [1.0; 3] {
1015 continue;
1016 }
1017 let rows = transform.rows;
1018 let to = geometry::placement(
1019 &mut self.emitter,
1020 cadmpeg_ir::math::Point3::new(rows[0][3], rows[1][3], rows[2][3]),
1021 cadmpeg_ir::math::Vector3::new(rows[0][2], rows[1][2], rows[2][2]),
1022 cadmpeg_ir::math::Vector3::new(rows[0][0], rows[1][0], rows[2][0]),
1023 );
1024 representation_placements
1025 .entry(parent_product.clone())
1026 .or_default()
1027 .push(to);
1028 occurrence_placements.insert(occurrence.id.clone(), (from, to));
1029 }
1030 let mut definitions = HashMap::<ProductDefinitionId, Ref>::new();
1031 let mut representations = HashMap::<ProductDefinitionId, Ref>::new();
1032 for product in products {
1033 let product_id = product
1034 .part_number
1035 .as_deref()
1036 .or(product.source_name.as_deref())
1037 .unwrap_or(product.id.as_str());
1038 let name = product
1039 .label
1040 .as_deref()
1041 .or(product.source_name.as_deref())
1042 .unwrap_or(product_id);
1043 let description = product.description.as_deref().unwrap_or("");
1044 let product_ref = self.emitter.emit(
1045 "PRODUCT",
1046 &format!(
1047 "{},{},{},({product_context})",
1048 string(product_id),
1049 string(name),
1050 string(description)
1051 ),
1052 );
1053 self.product_step_refs
1054 .insert(product.id.0.clone(), product_ref);
1055 let formation = self.emitter.emit(
1056 "PRODUCT_DEFINITION_FORMATION",
1057 &format!("'','',{product_ref}"),
1058 );
1059 let definition = self.emitter.emit(
1060 "PRODUCT_DEFINITION",
1061 &format!(
1062 "{},{},{formation},{definition_context}",
1063 string(product_id),
1064 string(description)
1065 ),
1066 );
1067 let shape = self
1068 .emitter
1069 .emit("PRODUCT_DEFINITION_SHAPE", &format!("'','',{definition}"));
1070 self.default_product_definition_shape.get_or_insert(shape);
1071 let mut body_items = product
1072 .bodies
1073 .iter()
1074 .flat_map(|body| {
1075 self.body_item_refs
1076 .get(body.as_str())
1077 .into_iter()
1078 .flatten()
1079 .copied()
1080 })
1081 .collect::<Vec<_>>();
1082 if let Some(origin) = product_origins.get(&product.id) {
1083 body_items.push(*origin);
1084 }
1085 if let Some(placements) = representation_placements.get(&product.id) {
1086 body_items.extend(placements);
1087 }
1088 let representation = self.emitter.emit(
1089 "SHAPE_REPRESENTATION",
1090 &format!("{},{},{context}", string(name), refs(&body_items)),
1091 );
1092 self.emitter.emit(
1093 "SHAPE_DEFINITION_REPRESENTATION",
1094 &format!("{shape},{representation}"),
1095 );
1096 definitions.insert(product.id.clone(), definition);
1097 representations.insert(product.id.clone(), representation);
1098 }
1099
1100 for occurrence in occurrences {
1101 let OccurrenceParent::Occurrence { occurrence: parent } = &occurrence.parent else {
1102 let transform = if occurrence.link_transform.unwrap_or(false) {
1103 occurrence.transform.compose(occurrence.prototype_transform)
1104 } else {
1105 occurrence.transform
1106 };
1107 if !is_identity(&transform.rows) || occurrence.scale != [1.0; 3] {
1108 self.loss(
1109 StepLossCode::RootOccurrencePlacementNotRepresentable,
1110 format!(
1111 "root occurrence '{}' has a placement or scale that is not representable",
1112 occurrence.id
1113 ),
1114 );
1115 }
1116 continue;
1117 };
1118 let Some(parent_occurrence) = graph.occurrence(parent) else {
1119 self.loss(
1120 StepLossCode::OccurrenceUnresolvedParent,
1121 format!("occurrence '{}' has an unresolved parent", occurrence.id),
1122 );
1123 continue;
1124 };
1125 let Some(parent_product) = occurrence_products.get(&parent_occurrence.id) else {
1126 self.missing_parent_products.insert(occurrence.id.0.clone());
1127 continue;
1128 };
1129 let Some(child_product) = occurrence_products.get(&occurrence.id) else {
1130 self.loss(
1131 StepLossCode::OccurrenceNoLocalProduct,
1132 format!(
1133 "occurrence '{}' has no local product definition",
1134 occurrence.id
1135 ),
1136 );
1137 continue;
1138 };
1139 let Some((
1140 &parent_definition,
1141 &child_definition,
1142 &parent_representation,
1143 &child_representation,
1144 )) = definitions
1145 .get(parent_product)
1146 .zip(definitions.get(child_product))
1147 .zip(representations.get(parent_product))
1148 .zip(representations.get(child_product))
1149 .map(|(((a, b), c), d)| (a, b, c, d))
1150 else {
1151 continue;
1152 };
1153 let transform = if occurrence.link_transform.unwrap_or(false) {
1154 occurrence.transform.compose(occurrence.prototype_transform)
1155 } else {
1156 occurrence.transform
1157 };
1158 if !transform.is_proper_rigid() || occurrence.scale != [1.0; 3] {
1159 self.loss(
1160 StepLossCode::OccurrencePlacementNotRigid,
1161 format!("occurrence '{}' placement is not rigid", occurrence.id),
1162 );
1163 continue;
1164 }
1165 let occurrence_name = occurrence.name.as_deref().unwrap_or(occurrence.id.as_str());
1166 let usage = self.emitter.emit(
1167 "NEXT_ASSEMBLY_USAGE_OCCURRENCE",
1168 &format!(
1169 "{},{},'',{parent_definition},{child_definition},$",
1170 string(occurrence.id.as_str()),
1171 string(occurrence_name)
1172 ),
1173 );
1174 self.occurrence_step_refs
1175 .insert(occurrence.id.0.clone(), usage);
1176 let usage_shape = self
1177 .emitter
1178 .emit("PRODUCT_DEFINITION_SHAPE", &format!("'','',{usage}"));
1179 let Some(&(from, to)) = occurrence_placements.get(&occurrence.id) else {
1180 continue;
1181 };
1182 let transform = self
1183 .emitter
1184 .emit("ITEM_DEFINED_TRANSFORMATION", &format!("'','',{from},{to}"));
1185 let relationship = self.emitter.emit_raw(
1186 "REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION",
1187 &format!(
1188 "( REPRESENTATION_RELATIONSHIP('','',{child_representation},{parent_representation}) REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION({transform}) SHAPE_REPRESENTATION_RELATIONSHIP() )"
1189 ),
1190 );
1191 self.emitter.emit(
1192 "CONTEXT_DEPENDENT_SHAPE_REPRESENTATION",
1193 &format!("{relationship},{usage_shape}"),
1194 );
1195 }
1196 }
1197
1198 fn emit_context(&mut self) -> Ref {
1199 let len = self.emit_length_unit();
1200 let angle = self.emit_angle_unit();
1201 let solid = self.emitter.emit_raw(
1202 "SOLID_ANGLE_UNIT",
1203 "( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() )",
1204 );
1205 let unc = self.emitter.emit(
1206 "UNCERTAINTY_MEASURE_WITH_UNIT",
1207 &format!(
1208 "LENGTH_MEASURE({}),{len},{},{}",
1209 real(self.ir.tolerances.linear),
1210 string("distance_accuracy_value"),
1211 string("maximum model space distance")
1212 ),
1213 );
1214 self.emitter.emit_raw(
1215 "GEOMETRIC_REPRESENTATION_CONTEXT",
1216 &format!(
1217 "( GEOMETRIC_REPRESENTATION_CONTEXT(3) \
1218 GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT(({unc})) \
1219 GLOBAL_UNIT_ASSIGNED_CONTEXT(({len},{angle},{solid})) \
1220 REPRESENTATION_CONTEXT('Context','3D') )"
1221 ),
1222 )
1223 }
1224
1225 fn emit_length_unit(&mut self) -> Ref {
1226 if let Some(unit) = self.length_unit {
1227 return unit;
1228 }
1229 let unit = self.emitter.emit_raw(
1230 "LENGTH_UNIT",
1231 "( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) )",
1232 );
1233 self.length_unit = Some(unit);
1234 unit
1235 }
1236
1237 fn emit_angle_unit(&mut self) -> Ref {
1238 if let Some(unit) = self.angle_unit {
1239 return unit;
1240 }
1241 let unit = self.emitter.emit_raw(
1242 "PLANE_ANGLE_UNIT",
1243 "( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) )",
1244 );
1245 self.angle_unit = Some(unit);
1246 unit
1247 }
1248
1249 fn emit_ratio_unit(&mut self) -> Ref {
1250 if let Some(unit) = self.ratio_unit {
1251 return unit;
1252 }
1253 let unit = self
1254 .emitter
1255 .emit_raw("RATIO_UNIT", "( NAMED_UNIT(*) RATIO_UNIT() )");
1256 self.ratio_unit = Some(unit);
1257 unit
1258 }
1259
1260 fn emit_shape_items(&mut self, context: Ref) -> Vec<Ref> {
1263 let mut items = Vec::new();
1264 let ir = self.ir;
1265 for region in &ir.model.regions {
1266 let body_kind = self
1267 .bodies
1268 .get(region.body.as_str())
1269 .map_or(BodyKind::General, |body| body.kind);
1270 let has_surface_topology = region.shells.iter().any(|shell_id| {
1271 self.shells
1272 .get(shell_id.as_str())
1273 .is_some_and(|shell| !shell.faces.is_empty())
1274 });
1275 let has_wire_topology = region.shells.iter().any(|shell_id| {
1276 self.shells.get(shell_id.as_str()).is_some_and(|shell| {
1277 !shell.wire_edges.is_empty() || !shell.free_vertices.is_empty()
1278 })
1279 });
1280 let mixed_wire = body_kind == BodyKind::General && has_wire_topology;
1281 if body_kind == BodyKind::Wire
1282 || (body_kind == BodyKind::General && !has_surface_topology)
1283 {
1284 if let Some(item) = self.emit_wire_region(region) {
1285 let shape_item = self.place_body_item(®ion.body, item, context);
1286 items.push(shape_item);
1287 self.body_shape_refs
1288 .entry(region.body.0.clone())
1289 .or_insert(shape_item);
1290 self.body_item_refs
1291 .entry(region.body.0.clone())
1292 .or_default()
1293 .push(shape_item);
1294 self.body_step_item_refs
1295 .entry(region.body.0.clone())
1296 .or_default()
1297 .push(item);
1298 self.body_step_refs
1299 .entry(region.body.0.clone())
1300 .or_insert(item);
1301 } else {
1302 self.empty_wire_regions.insert(region.id.0.clone());
1303 }
1304 continue;
1305 }
1306 let closed = body_kind == BodyKind::Solid;
1307 let Some((outer_id, void_ids)) = region.shells.split_first() else {
1308 self.empty_regions.insert(region.id.0.clone());
1309 continue;
1310 };
1311 let Some(outer) = self.emit_shell(outer_id.as_str(), closed) else {
1312 self.topology_relation_loss(
1313 format!("region:{}:outer-shell:{}", region.id, outer_id),
1314 StepLossCode::RegionNoWritableOuterShell,
1315 format!("region {} has no writable outer shell", region.id),
1316 );
1317 continue;
1318 };
1319 let mut voids = Vec::new();
1320 for sid in void_ids {
1321 if let Some(void) = self.emit_shell(sid.as_str(), closed) {
1322 voids.push(void);
1323 } else {
1324 self.topology_relation_loss(
1325 format!("region:{}:void-shell:{}", region.id, sid),
1326 StepLossCode::RegionOmittedVoidShell,
1327 format!(
1328 "region {} omitted void shell {} because it has no writable faces",
1329 region.id, sid
1330 ),
1331 );
1332 }
1333 }
1334 let mut shell_refs = Vec::with_capacity(1 + voids.len());
1335 shell_refs.push(outer);
1336 shell_refs.extend_from_slice(&voids);
1337 let item = if !closed {
1338 self.emitter.emit(
1339 "SHELL_BASED_SURFACE_MODEL",
1340 &format!("'',{}", refs(&shell_refs)),
1341 )
1342 } else if voids.is_empty() {
1343 self.emitter
1344 .emit("MANIFOLD_SOLID_BREP", &format!("'',{outer}"))
1345 } else {
1346 let void_refs: Vec<Ref> = voids
1347 .iter()
1348 .map(|s| {
1349 self.emitter
1350 .emit("ORIENTED_CLOSED_SHELL", &format!("'',*,{s},.F."))
1351 })
1352 .collect();
1353 self.emitter.emit(
1354 "BREP_WITH_VOIDS",
1355 &format!("'',{outer},{}", refs(&void_refs)),
1356 )
1357 };
1358 let shape_item = self.place_body_item(®ion.body, item, context);
1359 items.push(shape_item);
1360 self.body_shape_refs
1361 .entry(region.body.0.clone())
1362 .or_insert(shape_item);
1363 self.body_item_refs
1364 .entry(region.body.0.clone())
1365 .or_default()
1366 .push(shape_item);
1367 self.body_step_item_refs
1368 .entry(region.body.0.clone())
1369 .or_default()
1370 .push(item);
1371 self.body_step_refs
1372 .entry(region.body.0.clone())
1373 .or_insert(if closed { item } else { outer });
1374 if mixed_wire {
1375 if let Some(item) = self.emit_wire_region(region) {
1376 let shape_item = self.place_body_item(®ion.body, item, context);
1377 items.push(shape_item);
1378 self.body_shape_refs
1379 .entry(region.body.0.clone())
1380 .or_insert(shape_item);
1381 self.body_item_refs
1382 .entry(region.body.0.clone())
1383 .or_default()
1384 .push(shape_item);
1385 self.body_step_item_refs
1386 .entry(region.body.0.clone())
1387 .or_default()
1388 .push(item);
1389 self.body_step_refs
1390 .entry(region.body.0.clone())
1391 .or_insert(item);
1392 } else {
1393 self.empty_wire_regions.insert(region.id.0.clone());
1394 }
1395 }
1396 }
1397 items
1398 }
1399
1400 fn place_body_item(
1401 &mut self,
1402 body_id: &cadmpeg_ir::ids::BodyId,
1403 item: Ref,
1404 context: Ref,
1405 ) -> Ref {
1406 let transform = self
1407 .bodies
1408 .get(body_id.as_str())
1409 .and_then(|body| body.transform);
1410 let Some(transform) = transform.filter(|transform| !is_identity(&transform.rows)) else {
1411 return item;
1412 };
1413 if !is_rigid_transform(&transform.rows) {
1414 self.loss(
1415 StepLossCode::BodyNonRigidTransform,
1416 format!("body '{body_id}' carries a non-rigid transform"),
1417 );
1418 return item;
1419 }
1420 let origin = geometry::placement(
1421 &mut self.emitter,
1422 cadmpeg_ir::math::Point3::new(0.0, 0.0, 0.0),
1423 cadmpeg_ir::math::Vector3::new(0.0, 0.0, 1.0),
1424 cadmpeg_ir::math::Vector3::new(1.0, 0.0, 0.0),
1425 );
1426 let representation = self.emitter.emit(
1427 "SHAPE_REPRESENTATION",
1428 &format!("'body-local',({item}),{context}"),
1429 );
1430 let map = self
1431 .emitter
1432 .emit("REPRESENTATION_MAP", &format!("{origin},{representation}"));
1433 let rows = transform.rows;
1434 let target = geometry::placement(
1435 &mut self.emitter,
1436 cadmpeg_ir::math::Point3::new(rows[0][3], rows[1][3], rows[2][3]),
1437 cadmpeg_ir::math::Vector3::new(rows[0][2], rows[1][2], rows[2][2]),
1438 cadmpeg_ir::math::Vector3::new(rows[0][0], rows[1][0], rows[2][0]),
1439 );
1440 self.emitter.emit(
1441 "MAPPED_ITEM",
1442 &format!("'cadmpeg body placement',{map},{target}"),
1443 )
1444 }
1445
1446 fn emit_visibility(&mut self) {
1447 if !self.schema.supports_visibility() {
1448 let hidden = self
1449 .ir
1450 .model
1451 .bodies
1452 .iter()
1453 .filter(|body| body.visible == Some(false))
1454 .count();
1455 if hidden != 0 {
1456 self.loss(
1457 StepLossCode::HiddenBodyVisibilityUnsupported,
1458 format!(
1459 "{hidden} hidden body visibility assignment(s) are unsupported by {}",
1460 self.schema.file_schema()
1461 ),
1462 );
1463 }
1464 return;
1465 }
1466 let mut hidden = Vec::new();
1467 let mut hidden_without_items = Vec::new();
1468 for body in &self.ir.model.bodies {
1469 if body.visible != Some(false) {
1470 continue;
1471 }
1472 if let Some(references) = self.body_step_item_refs.get(body.id.as_str()) {
1473 if !references.is_empty() {
1474 hidden.extend(references.iter().copied());
1475 continue;
1476 }
1477 }
1478 if let Some(reference) = self.body_step_refs.get(body.id.as_str()).copied() {
1479 hidden.push(reference);
1480 } else {
1481 hidden_without_items.push(body.id.0.clone());
1482 }
1483 }
1484 self.hidden_bodies_without_items
1485 .extend(hidden_without_items);
1486 if !hidden.is_empty() {
1487 self.emitter.emit("INVISIBILITY", &refs(&hidden));
1488 }
1489 }
1490
1491 fn emit_wire_region(&mut self, region: &cadmpeg_ir::topology::Region) -> Option<Ref> {
1492 let mut shells = Vec::new();
1493 for shell_id in ®ion.shells {
1494 if let Some(shell) = self.shells.get(shell_id.as_str()).copied().cloned() {
1495 shells.push(shell);
1496 } else {
1497 self.missing_wire_shells
1498 .insert((region.id.0.clone(), shell_id.0.clone()));
1499 }
1500 }
1501 let mut connected_sets = Vec::new();
1502 for shell in shells {
1503 if !shell.free_vertices.is_empty() {
1504 self.loss(
1505 StepLossCode::WireShellFreeVertices,
1506 format!(
1507 "wire shell '{}' has {} free vertex/vertices without an edge-based STEP carrier",
1508 shell.id,
1509 shell.free_vertices.len()
1510 ),
1511 );
1512 }
1513 let edges = shell
1514 .wire_edges
1515 .iter()
1516 .filter_map(|edge| self.emit_edge(edge.as_str()))
1517 .collect::<Vec<_>>();
1518 if !edges.is_empty() {
1519 connected_sets.push(
1520 self.emitter
1521 .emit("CONNECTED_EDGE_SET", &format!("'',{}", refs(&edges))),
1522 );
1523 }
1524 }
1525 if connected_sets.is_empty() {
1526 return None;
1527 }
1528 Some(self.emitter.emit(
1529 "EDGE_BASED_WIREFRAME_MODEL",
1530 &format!("'',{}", refs(&connected_sets)),
1531 ))
1532 }
1533
1534 fn emit_standalone_geometry(&mut self) -> Vec<Ref> {
1535 let surface_ids = self
1536 .ir
1537 .model
1538 .surfaces
1539 .iter()
1540 .filter(|surface| !self.surface_refs.contains_key(surface.id.as_str()))
1541 .map(|surface| surface.id.0.clone())
1542 .collect::<Vec<_>>();
1543 let mut members = Vec::new();
1544 let mut has_surfaces = false;
1545 for surface_id in surface_ids {
1546 if let Some(reference) = self.emit_surface(&surface_id) {
1547 members.push(reference);
1548 has_surfaces = true;
1549 } else {
1550 self.unwritten_geometry_carriers.insert(surface_id);
1551 }
1552 }
1553 let curve_ids = self
1554 .ir
1555 .model
1556 .curves
1557 .iter()
1558 .filter(|curve| !self.curve_refs.contains_key(curve.id.as_str()))
1559 .map(|curve| curve.id.0.clone())
1560 .collect::<Vec<_>>();
1561 for curve_id in curve_ids {
1562 if let Some(reference) = self.emit_curve(&curve_id) {
1563 members.push(reference);
1564 } else {
1565 self.unwritten_geometry_carriers.insert(curve_id);
1566 }
1567 }
1568 let point_ids = self
1569 .ir
1570 .model
1571 .points
1572 .iter()
1573 .filter(|point| !self.point_refs.contains_key(point.id.as_str()))
1574 .map(|point| point.id.0.clone())
1575 .collect::<Vec<_>>();
1576 for point_id in point_ids {
1577 let Some(point) = self.points.get(point_id.as_str()).copied() else {
1578 continue;
1579 };
1580 let reference = geometry::point(&mut self.emitter, point.position);
1581 self.point_refs.insert(point_id, reference);
1582 members.push(reference);
1583 }
1584 if members.is_empty() {
1585 Vec::new()
1586 } else {
1587 vec![self.emitter.emit(
1588 if has_surfaces {
1589 "GEOMETRIC_SET"
1590 } else {
1591 "GEOMETRIC_CURVE_SET"
1592 },
1593 &format!("'',{}", refs(&members)),
1594 )]
1595 }
1596 }
1597
1598 fn emit_tessellations(&mut self, context: Ref) {
1599 if self.ir.model.tessellations.is_empty() {
1600 return;
1601 }
1602 if !self.schema.supports_tessellation() {
1603 self.loss(
1604 StepLossCode::TessellationRequiresAp242,
1605 format!(
1606 "{} tessellation(s) require an AP242 target",
1607 self.ir.model.tessellations.len()
1608 ),
1609 );
1610 return;
1611 }
1612
1613 let ir = self.ir;
1614 let mut representation_items = Vec::new();
1615 for mesh in &ir.model.tessellations {
1616 if !mesh.feature_edges.is_empty() {
1617 self.loss(
1618 StepLossCode::TessellationFeatureEdges,
1619 format!(
1620 "tessellation '{}' feature-edge classification is not represented",
1621 mesh.id
1622 ),
1623 );
1624 }
1625 if !mesh.corner_normals.is_empty() {
1626 self.loss(
1627 StepLossCode::TessellationCornerNormals,
1628 format!(
1629 "tessellation '{}' corner normals are not represented",
1630 mesh.id
1631 ),
1632 );
1633 }
1634 if !mesh.triangle_groups.is_empty() {
1635 self.loss(
1636 StepLossCode::TessellationTriangleGroups,
1637 format!(
1638 "tessellation '{}' triangle groups are not represented",
1639 mesh.id
1640 ),
1641 );
1642 }
1643 if !mesh.texture_assignments.is_empty() {
1644 self.loss(
1645 StepLossCode::TessellationTextureAssignments,
1646 format!(
1647 "tessellation '{}' texture assignments are not represented",
1648 mesh.id
1649 ),
1650 );
1651 }
1652 if mesh.vertices.is_empty()
1653 || mesh.triangles.is_empty()
1654 || mesh
1655 .triangles
1656 .iter()
1657 .flatten()
1658 .any(|index| *index as usize >= mesh.vertices.len())
1659 || (!mesh.normals.is_empty() && mesh.normals.len() != mesh.vertices.len())
1660 {
1661 self.loss(
1662 StepLossCode::TessellationInvalidCardinality,
1663 format!(
1664 "tessellation '{}' has invalid vertex/index/normal cardinality",
1665 mesh.id
1666 ),
1667 );
1668 continue;
1669 }
1670 let coordinates = mesh
1671 .vertices
1672 .iter()
1673 .map(|point| format!("({},{},{})", real(point.x), real(point.y), real(point.z)))
1674 .collect::<Vec<_>>()
1675 .join(",");
1676 let coordinates = self.emitter.emit(
1677 "COORDINATES_LIST",
1678 &format!(
1679 "{}, {},({coordinates})",
1680 string(&mesh.id),
1681 mesh.vertices.len()
1682 ),
1683 );
1684 let normals = if mesh.normals.is_empty() {
1685 "$".to_string()
1686 } else {
1687 format!(
1688 "({})",
1689 mesh.normals
1690 .iter()
1691 .map(|normal| format!(
1692 "({},{},{})",
1693 real(normal.x),
1694 real(normal.y),
1695 real(normal.z)
1696 ))
1697 .collect::<Vec<_>>()
1698 .join(",")
1699 )
1700 };
1701 let point_indices = (1..=mesh.vertices.len())
1702 .map(|index| index.to_string())
1703 .collect::<Vec<_>>()
1704 .join(",");
1705 let linked_body = mesh.body.as_ref().and_then(|body| {
1706 let link = self.body_step_refs.get(body.as_str()).copied()?;
1707 let kind = self.bodies.get(body.as_str())?.kind;
1708 matches!(kind, BodyKind::Solid | BodyKind::Sheet).then_some((kind, link))
1709 });
1710 if let Some(body) = &mesh.body {
1711 if linked_body.is_none() {
1712 self.loss(
1713 StepLossCode::TessellationBodyLinkUnwritable,
1714 format!(
1715 "tessellation '{}' body '{}' has no writable AP242 tessellation link",
1716 mesh.id, body
1717 ),
1718 );
1719 }
1720 }
1721 let mut reduced_fields = Vec::new();
1722 if !mesh.faces.is_empty() {
1723 reduced_fields.push(format!("{} face ownership link(s)", mesh.faces.len()));
1724 }
1725 if mesh.chordal_deflection.is_some() {
1726 reduced_fields.push("chordal deflection".to_string());
1727 }
1728 if !mesh.channels.is_empty() {
1729 reduced_fields.push(format!("{} data channel(s)", mesh.channels.len()));
1730 }
1731 if !reduced_fields.is_empty() {
1732 self.loss(
1733 StepLossCode::TessellationMetadataReduced,
1734 format!(
1735 "tessellation '{}' reduced unsupported metadata: {}",
1736 mesh.id,
1737 reduced_fields.join(", ")
1738 ),
1739 );
1740 }
1741 let item = if let Some((kind, link)) = linked_body {
1742 let triangles = mesh
1743 .triangles
1744 .iter()
1745 .map(|triangle| {
1746 format!(
1747 "({},{},{})",
1748 triangle[0] + 1,
1749 triangle[1] + 1,
1750 triangle[2] + 1
1751 )
1752 })
1753 .collect::<Vec<_>>()
1754 .join(",");
1755 let face = self.emitter.emit(
1756 "TRIANGULATED_FACE",
1757 &format!(
1758 "{},{coordinates},{},{normals},$,({point_indices}),({triangles})",
1759 string(&mesh.id),
1760 mesh.vertices.len()
1761 ),
1762 );
1763 self.emitter.emit(
1764 if kind == BodyKind::Solid {
1765 "TESSELLATED_SOLID"
1766 } else {
1767 "TESSELLATED_SHELL"
1768 },
1769 &format!("{},({face}),{link}", string(&mesh.id)),
1770 )
1771 } else {
1772 let triangles = mesh
1773 .triangles
1774 .iter()
1775 .map(|triangle| {
1776 format!(
1777 "({},{},{})",
1778 triangle[0] + 1,
1779 triangle[1] + 1,
1780 triangle[2] + 1
1781 )
1782 })
1783 .collect::<Vec<_>>()
1784 .join(",");
1785 self.emitter.emit(
1786 "TRIANGULATED_SURFACE_SET",
1787 &format!(
1788 "{},{coordinates},{},{normals},({point_indices}),({triangles})",
1789 string(&mesh.id),
1790 mesh.vertices.len()
1791 ),
1792 )
1793 };
1794 self.tessellation_step_refs.insert(mesh.id.clone(), item);
1795 representation_items.push(item);
1796 }
1797 if !representation_items.is_empty() {
1798 self.emitter.emit(
1799 "TESSELLATED_SHAPE_REPRESENTATION",
1800 &format!("'',{},{context}", refs(&representation_items)),
1801 );
1802 }
1803 }
1804
1805 fn emit_shell(&mut self, shell_id: &str, closed: bool) -> Option<Ref> {
1806 let shell = self.shells.get(shell_id).copied()?;
1807 let face_ids: Vec<String> = shell.faces.iter().map(|f| f.0.clone()).collect();
1808 let mut face_refs = Vec::new();
1809 for fid in &face_ids {
1810 if let Some(r) = self.emit_face(fid) {
1811 face_refs.push(r);
1812 } else {
1813 let outer = self.faces.get(fid.as_str()).is_some_and(|face| {
1814 face.loops.iter().any(|loop_id| {
1815 self.loops
1816 .get(loop_id.as_str())
1817 .is_some_and(|loop_| loop_.boundary_role == LoopBoundaryRole::Outer)
1818 }) || face.loops.first().is_some_and(|loop_id| {
1819 !face.loops.iter().any(|candidate| {
1820 self.loops
1821 .get(candidate.as_str())
1822 .is_some_and(|loop_| loop_.boundary_role == LoopBoundaryRole::Outer)
1823 }) && self.loops.contains_key(loop_id.as_str())
1824 })
1825 });
1826 self.topology_relation_loss(
1827 format!("shell:{shell_id}:face:{fid}"),
1828 if outer {
1829 StepLossCode::ShellOmittedOuterFace
1830 } else {
1831 StepLossCode::ShellOmittedInnerFace
1832 },
1833 format!(
1834 "shell {shell_id} omitted face {fid} because the face has no writable topology"
1835 ),
1836 );
1837 }
1838 }
1839 if face_refs.is_empty() {
1840 return None;
1841 }
1842 Some(self.emitter.emit(
1843 if closed { "CLOSED_SHELL" } else { "OPEN_SHELL" },
1844 &format!("'',{}", refs(&face_refs)),
1845 ))
1846 }
1847
1848 fn emit_face(&mut self, face_id: &str) -> Option<Ref> {
1849 let face = self.faces.get(face_id).copied()?;
1850 let surface_id = face.surface.0.clone();
1851 if let Some(surf) = self.surfaces.get(surface_id.as_str()) {
1855 if !geometry::surface_is_supported(&surf.geometry) {
1856 self.unknown_surface_faces.insert(face_id.to_string());
1857 return None;
1858 }
1859 }
1860 let loop_ids: Vec<String> = face.loops.iter().map(|l| l.0.clone()).collect();
1861 let same_sense = matches!(face.sense, Sense::Forward);
1862
1863 let Some(surf_ref) = self.emit_surface(&surface_id) else {
1864 self.unknown_surface_faces.insert(face_id.to_string());
1865 return None;
1866 };
1867
1868 let mut bound_refs = Vec::new();
1869 for (i, lid) in loop_ids.iter().enumerate() {
1870 if let Some(loop_ref) = self.emit_loop(lid) {
1871 let kind = if matches!(
1872 self.loops
1873 .get(lid.as_str())
1874 .map(|loop_| loop_.boundary_role),
1875 Some(LoopBoundaryRole::Outer)
1876 ) || (i == 0
1877 && !loop_ids.iter().any(|id| {
1878 self.loops
1879 .get(id.as_str())
1880 .is_some_and(|loop_| loop_.boundary_role == LoopBoundaryRole::Outer)
1881 })) {
1882 "FACE_OUTER_BOUND"
1883 } else {
1884 "FACE_BOUND"
1885 };
1886 let b = self.emitter.emit(kind, &format!("'',{loop_ref},.T."));
1887 bound_refs.push(b);
1888 } else {
1889 let outer = self
1890 .loops
1891 .get(lid.as_str())
1892 .is_some_and(|loop_| loop_.boundary_role == LoopBoundaryRole::Outer)
1893 || (i == 0
1894 && !loop_ids.iter().any(|id| {
1895 self.loops
1896 .get(id.as_str())
1897 .is_some_and(|loop_| loop_.boundary_role == LoopBoundaryRole::Outer)
1898 }));
1899 self.topology_relation_loss(
1900 format!("face:{face_id}:loop:{lid}"),
1901 if outer {
1902 StepLossCode::FaceOmittedOuterLoop
1903 } else {
1904 StepLossCode::FaceOmittedInnerLoop
1905 },
1906 format!(
1907 "face {face_id} omitted loop {lid} because the loop has no writable topology"
1908 ),
1909 );
1910 }
1911 }
1912 if bound_refs.is_empty() {
1913 self.topology_relation_loss(
1914 format!("face:{face_id}:bound-list"),
1915 StepLossCode::FaceNoWritableBounds,
1916 format!("face {face_id} has no writable bounds"),
1917 );
1918 return None;
1919 }
1920 let flag = if same_sense { ".T." } else { ".F." };
1921 let advanced_face = self.emitter.emit(
1922 "ADVANCED_FACE",
1923 &format!(
1924 "{},{},{surf_ref},{flag}",
1925 string(face.name.as_deref().unwrap_or("")),
1926 refs(&bound_refs)
1927 ),
1928 );
1929 self.face_step_refs
1930 .insert(face_id.to_string(), advanced_face);
1931 Some(advanced_face)
1932 }
1933
1934 fn emit_loop(&mut self, loop_id: &str) -> Option<Ref> {
1935 let Some(lp) = self.loops.get(loop_id).copied() else {
1936 self.topology_relation_loss(
1937 format!("loop:{loop_id}:record"),
1938 StepLossCode::LoopRecordMissing,
1939 format!("loop {loop_id} was omitted because its record is missing"),
1940 );
1941 return None;
1942 };
1943 if lp.coedges.is_empty() && lp.vertex_uses.len() == 1 {
1944 let vertex_id = lp.vertex_uses[0].vertex.as_str();
1945 let Some(vertex) = self.emit_vertex(vertex_id) else {
1946 self.topology_relation_loss(
1947 format!("loop:{loop_id}:vertex:{vertex_id}"),
1948 StepLossCode::LoopVertexMissing,
1949 format!("loop {loop_id} was omitted because vertex {vertex_id} is missing"),
1950 );
1951 return None;
1952 };
1953 return Some(self.emitter.emit("VERTEX_LOOP", &format!("'',{vertex}")));
1954 }
1955 let coedge_ids = self.ordered_loop_coedges(loop_id, lp)?;
1956 let mut oe_refs = Vec::new();
1957 for cid in &coedge_ids {
1958 let Some(coedge) = self.coedges.get(cid.as_str()).copied() else {
1959 self.topology_relation_loss(
1960 format!("loop:{loop_id}:coedge:{cid}"),
1961 StepLossCode::LoopCoedgeRecordMissing,
1962 format!("loop {loop_id} omitted coedge {cid} because its record is missing"),
1963 );
1964 return None;
1965 };
1966 let orientation = matches!(coedge.sense, Sense::Forward);
1967 let Some(edge_ref) = self.emit_edge(coedge.edge.as_str()) else {
1968 self.topology_relation_loss(
1969 format!("loop:{loop_id}:edge:{}", coedge.edge),
1970 StepLossCode::LoopEdgeNotWritable,
1971 format!(
1972 "loop {loop_id} omitted edge {} because the edge is not writable",
1973 coedge.edge
1974 ),
1975 );
1976 return None;
1977 };
1978 let flag = if orientation { ".T." } else { ".F." };
1979 let oe = self
1980 .emitter
1981 .emit("ORIENTED_EDGE", &format!("'',*,*,{edge_ref},{flag}"));
1982 oe_refs.push(oe);
1983 }
1984 if oe_refs.is_empty() {
1985 self.topology_relation_loss(
1986 format!("loop:{loop_id}:edge-list"),
1987 StepLossCode::LoopNoWritableEdges,
1988 format!("loop {loop_id} has no writable edges"),
1989 );
1990 return None;
1991 }
1992 Some(
1993 self.emitter
1994 .emit("EDGE_LOOP", &format!("'',{}", refs(&oe_refs))),
1995 )
1996 }
1997
1998 fn ordered_loop_coedges(&mut self, loop_id: &str, lp: &Loop) -> Option<Vec<String>> {
1999 let mut segments = Vec::with_capacity(lp.coedges.len());
2000 let mut seen = BTreeSet::new();
2001
2002 for coedge_id in &lp.coedges {
2003 let coedge_key = coedge_id.as_str();
2004 if !seen.insert(coedge_key) {
2005 self.topology_relation_loss(
2006 format!("loop:{loop_id}:duplicate-coedge:{coedge_id}"),
2007 StepLossCode::LoopDuplicateCoedge,
2008 format!(
2009 "loop {loop_id} cannot be ordered because coedge {coedge_id} occurs more than once"
2010 ),
2011 );
2012 return None;
2013 }
2014 let Some(coedge) = self.coedges.get(coedge_key).copied() else {
2015 self.topology_relation_loss(
2016 format!("loop:{loop_id}:coedge:{coedge_id}"),
2017 StepLossCode::LoopCoedgeMissingForOrder,
2018 format!(
2019 "loop {loop_id} cannot be ordered because coedge {coedge_id} is missing"
2020 ),
2021 );
2022 return None;
2023 };
2024 let edge_key = coedge.edge.as_str();
2025 let Some(edge) = self.edges.get(edge_key).copied() else {
2026 self.topology_relation_loss(
2027 format!("loop:{loop_id}:edge:{edge_key}"),
2028 StepLossCode::LoopEdgeMissingForOrder,
2029 format!("loop {loop_id} cannot be ordered because edge {edge_key} is missing"),
2030 );
2031 return None;
2032 };
2033 let Some(curve_id) = edge.curve.as_ref() else {
2034 self.curveless_edges.insert(edge_key.to_string());
2035 self.topology_relation_loss(
2036 format!("edge:{edge_key}:curve"),
2037 StepLossCode::EdgeNo3dCurve,
2038 format!("edge {edge_key} was omitted because it has no 3D curve"),
2039 );
2040 continue;
2041 };
2042 let Some(curve) = self.curves.get(curve_id.as_str()).copied() else {
2043 self.topology_relation_loss(
2044 format!("edge:{edge_key}:curve:{curve_id}"),
2045 StepLossCode::EdgeCurveNotWritable,
2046 format!("edge {edge_key} was omitted because curve {curve_id} is not writable"),
2047 );
2048 continue;
2049 };
2050 if !geometry::curve_is_supported(&curve.geometry) {
2051 self.curveless_edges.insert(edge_key.to_string());
2052 self.topology_relation_loss(
2053 format!("edge:{edge_key}:unsupported-curve:{curve_id}"),
2054 StepLossCode::EdgeCurveUnsupported,
2055 format!("edge {edge_key} was omitted because curve {curve_id} is unsupported"),
2056 );
2057 continue;
2058 }
2059 let (start_vertex, end_vertex) = match coedge.sense {
2060 Sense::Forward => (&edge.start, &edge.end),
2061 Sense::Reversed => (&edge.end, &edge.start),
2062 };
2063 segments.push(LoopSegment {
2064 coedge_id: coedge_id.0.clone(),
2065 start_vertex: start_vertex.0.clone(),
2066 end_vertex: end_vertex.0.clone(),
2067 });
2068 }
2069
2070 let mut outgoing: HashMap<String, Vec<usize>> = HashMap::new();
2071 let mut incoming: HashMap<&str, usize> = HashMap::new();
2072 for (index, segment) in segments.iter().enumerate() {
2073 outgoing
2074 .entry(segment.start_vertex.clone())
2075 .or_default()
2076 .push(index);
2077 *incoming.entry(segment.end_vertex.as_str()).or_default() += 1;
2078 }
2079 if segments.iter().any(|segment| {
2080 outgoing.get(&segment.start_vertex).map_or(0, Vec::len)
2081 != incoming
2082 .get(segment.start_vertex.as_str())
2083 .copied()
2084 .unwrap_or(0)
2085 }) {
2086 self.topology_relation_loss(
2087 format!("loop:{loop_id}:continuity"),
2088 StepLossCode::LoopNoContinuousOrdering,
2089 format!("loop {loop_id} has no continuous vertex-to-vertex coedge ordering"),
2090 );
2091 return None;
2092 }
2093
2094 for candidates in outgoing.values_mut() {
2099 candidates.reverse();
2100 }
2101 let first_start = segments.first()?.start_vertex.clone();
2102 let mut vertex_stack = vec![first_start.clone()];
2103 let mut edge_stack = Vec::new();
2104 let mut circuit = Vec::with_capacity(segments.len());
2105 while let Some(vertex) = vertex_stack.last().cloned() {
2106 let next_edge = outgoing.get_mut(&vertex).and_then(Vec::pop);
2107 if let Some(edge_index) = next_edge {
2108 vertex_stack.push(segments[edge_index].end_vertex.clone());
2109 edge_stack.push(edge_index);
2110 } else {
2111 vertex_stack.pop();
2112 if let Some(edge_index) = edge_stack.pop() {
2113 circuit.push(edge_index);
2114 }
2115 }
2116 }
2117 circuit.reverse();
2118
2119 if circuit.len() != segments.len() {
2120 self.topology_relation_loss(
2121 format!("loop:{loop_id}:continuity"),
2122 StepLossCode::LoopNoContinuousOrdering,
2123 format!("loop {loop_id} has no continuous vertex-to-vertex coedge ordering"),
2124 );
2125 return None;
2126 }
2127
2128 if let Some(first_index) = circuit.iter().position(|index| *index == 0) {
2129 circuit.rotate_left(first_index);
2130 }
2131 let mut current_vertex = segments.first()?.start_vertex.as_str();
2132 for edge_index in &circuit {
2133 let segment = &segments[*edge_index];
2134 if segment.start_vertex != current_vertex {
2135 self.topology_relation_loss(
2136 format!("loop:{loop_id}:continuity"),
2137 StepLossCode::LoopNoContinuousOrdering,
2138 format!("loop {loop_id} has no continuous vertex-to-vertex coedge ordering"),
2139 );
2140 return None;
2141 }
2142 current_vertex = segment.end_vertex.as_str();
2143 }
2144 if current_vertex != first_start {
2145 self.topology_relation_loss(
2146 format!("loop:{loop_id}:continuity"),
2147 StepLossCode::LoopNoContinuousOrdering,
2148 format!("loop {loop_id} has no continuous vertex-to-vertex coedge ordering"),
2149 );
2150 return None;
2151 }
2152
2153 Some(
2154 circuit
2155 .into_iter()
2156 .map(|index| segments[index].coedge_id.clone())
2157 .collect(),
2158 )
2159 }
2160
2161 fn emit_edge(&mut self, edge_id: &str) -> Option<Ref> {
2162 if let Some(r) = self.edge_refs.get(edge_id) {
2163 return Some(*r);
2164 }
2165 let Some(edge) = self.edges.get(edge_id).copied() else {
2166 self.topology_relation_loss(
2167 format!("edge:{edge_id}:record"),
2168 StepLossCode::EdgeRecordMissing,
2169 format!("edge {edge_id} was omitted because its record is missing"),
2170 );
2171 return None;
2172 };
2173 let Some(v1) = self.emit_vertex(edge.start.as_str()) else {
2174 self.topology_relation_loss(
2175 format!("edge:{edge_id}:start-vertex:{}", edge.start),
2176 StepLossCode::EdgeStartVertexMissing,
2177 format!(
2178 "edge {edge_id} was omitted because start vertex {} is missing",
2179 edge.start
2180 ),
2181 );
2182 return None;
2183 };
2184 let Some(v2) = self.emit_vertex(edge.end.as_str()) else {
2185 self.topology_relation_loss(
2186 format!("edge:{edge_id}:end-vertex:{}", edge.end),
2187 StepLossCode::EdgeEndVertexMissing,
2188 format!(
2189 "edge {edge_id} was omitted because end vertex {} is missing",
2190 edge.end
2191 ),
2192 );
2193 return None;
2194 };
2195 let Some(curve_id) = &edge.curve else {
2196 self.curveless_edges.insert(edge_id.to_string());
2197 self.topology_relation_loss(
2198 format!("edge:{edge_id}:curve"),
2199 StepLossCode::EdgeNo3dCurve,
2200 format!("edge {edge_id} was omitted because it has no 3D curve"),
2201 );
2202 return None;
2203 };
2204 if self
2205 .curves
2206 .get(curve_id.as_str())
2207 .is_some_and(|curve| !geometry::curve_is_supported(&curve.geometry))
2208 {
2209 self.curveless_edges.insert(edge_id.to_string());
2210 self.topology_relation_loss(
2211 format!("edge:{edge_id}:unsupported-curve:{curve_id}"),
2212 StepLossCode::EdgeCurveUnsupported,
2213 format!("edge {edge_id} was omitted because curve {curve_id} is unsupported"),
2214 );
2215 return None;
2216 }
2217 let Some(basis_curve) = self.emit_curve(curve_id.as_str()) else {
2218 self.topology_relation_loss(
2219 format!("edge:{edge_id}:curve:{curve_id}"),
2220 StepLossCode::EdgeCurveNotWritable,
2221 format!("edge {edge_id} was omitted because curve {curve_id} is not writable"),
2222 );
2223 return None;
2224 };
2225 let associated = self.edge_coedges.get(edge_id).cloned().unwrap_or_default();
2226 let mut pcurve_refs = Vec::new();
2227 for (pcurve_id, surface_id) in associated {
2228 if let Some(pcurve) = self.emit_pcurve(pcurve_id, surface_id) {
2229 pcurve_refs.push(pcurve);
2230 } else if self.pcurves.contains_key(pcurve_id) {
2231 self.unwritten_pcurve_carriers.insert(pcurve_id.to_string());
2232 }
2233 }
2234 let curve_ref = if pcurve_refs.is_empty() {
2235 basis_curve
2236 } else {
2237 self.emitter.emit(
2238 "SURFACE_CURVE",
2239 &format!("'',{basis_curve},{},.CURVE_3D.", refs(&pcurve_refs)),
2240 )
2241 };
2242 let r = self
2245 .emitter
2246 .emit("EDGE_CURVE", &format!("'',{v1},{v2},{curve_ref},.T."));
2247 self.edge_refs.insert(edge_id.to_string(), r);
2248 Some(r)
2249 }
2250
2251 fn emit_pcurve(&mut self, pcurve_id: &str, surface_id: &str) -> Option<Ref> {
2252 let pcurve = self.pcurves.get(pcurve_id).copied()?;
2253 let surface = self.emit_surface(surface_id)?;
2254 let curve = geometry::pcurve(&mut self.emitter, &pcurve.geometry)?;
2255 let context = if let Some(context) = self.pcurve_context {
2256 context
2257 } else {
2258 let context = self.emitter.emit_raw(
2259 "GEOMETRIC_REPRESENTATION_CONTEXT",
2260 "( GEOMETRIC_REPRESENTATION_CONTEXT(2) PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('uv','2D') )",
2261 );
2262 self.pcurve_context = Some(context);
2263 context
2264 };
2265 let representation = self.emitter.emit(
2266 "DEFINITIONAL_REPRESENTATION",
2267 &format!("'',({curve}),{context}"),
2268 );
2269 Some(
2270 self.emitter
2271 .emit("PCURVE", &format!("'',{surface},{representation}")),
2272 )
2273 }
2274
2275 fn emit_vertex(&mut self, vertex_id: &str) -> Option<Ref> {
2276 if let Some(r) = self.vertex_refs.get(vertex_id) {
2277 return Some(*r);
2278 }
2279 let vertex = self.vertices.get(vertex_id).copied()?;
2280 let pt = self.points.get(vertex.point.as_str()).copied()?;
2281 let cp = geometry::point(&mut self.emitter, pt.position);
2282 self.point_refs.insert(vertex.point.0.clone(), cp);
2283 let r = self.emitter.emit("VERTEX_POINT", &format!("'',{cp}"));
2284 self.vertex_refs.insert(vertex_id.to_string(), r);
2285 Some(r)
2286 }
2287
2288 fn emit_surface(&mut self, surface_id: &str) -> Option<Ref> {
2289 if let Some(r) = self.surface_refs.get(surface_id) {
2290 return Some(*r);
2291 }
2292 if self.geometry_emission_depth >= 256
2293 || !self.active_surfaces.insert(surface_id.to_string())
2294 {
2295 return None;
2296 }
2297 self.geometry_emission_depth += 1;
2298 let result = (|| {
2299 let surf = self.surfaces.get(surface_id).copied()?;
2300 let procedural = self
2301 .procedural_surfaces
2302 .get(surface_id)
2303 .map(|procedural| (procedural.id.0.clone(), procedural.definition.clone()));
2304 let emitted = procedural.and_then(|(id, definition)| {
2305 self.emit_procedural_surface(&surf.geometry, &definition)
2306 .map(|reference| (id, reference))
2307 });
2308 let r = if let Some((id, reference)) = emitted {
2309 self.written_procedural_surfaces.insert(id);
2310 reference
2311 } else if !geometry::surface_is_supported(&surf.geometry) {
2312 return None;
2313 } else {
2314 geometry::surface(&mut self.emitter, &surf.geometry)?
2315 };
2316 Some(r)
2317 })();
2318 self.active_surfaces.remove(surface_id);
2319 self.geometry_emission_depth -= 1;
2320 if let Some(r) = result {
2321 self.surface_refs.insert(surface_id.to_string(), r);
2322 }
2323 result
2324 }
2325
2326 fn emit_procedural_surface(
2327 &mut self,
2328 solved: &SurfaceGeometry,
2329 definition: &ProceduralSurfaceDefinition,
2330 ) -> Option<Ref> {
2331 let logical = |value: Option<bool>| match value {
2332 Some(true) => ".T.",
2333 Some(false) => ".F.",
2334 None => ".U.",
2335 };
2336 match definition {
2337 ProceduralSurfaceDefinition::LinearSweep {
2338 directrix,
2339 direction,
2340 } => {
2341 let directrix = self.emit_curve(directrix.as_str())?;
2342 let direction_ref = geometry::direction(&mut self.emitter, *direction);
2343 let vector = self.emitter.emit(
2344 "VECTOR",
2345 &format!("'',{direction_ref},{}", real(direction.norm())),
2346 );
2347 Some(self.emitter.emit(
2348 "SURFACE_OF_LINEAR_EXTRUSION",
2349 &format!("'',{directrix},{vector}"),
2350 ))
2351 }
2352 ProceduralSurfaceDefinition::AxisRevolution {
2353 directrix,
2354 axis_origin,
2355 axis_direction,
2356 } => {
2357 let directrix = self.emit_curve(directrix.as_str())?;
2358 let origin = geometry::point(&mut self.emitter, *axis_origin);
2359 let direction = geometry::direction(&mut self.emitter, *axis_direction);
2360 let axis = self
2361 .emitter
2362 .emit("AXIS1_PLACEMENT", &format!("'',{origin},{direction}"));
2363 Some(
2364 self.emitter
2365 .emit("SURFACE_OF_REVOLUTION", &format!("'',{directrix},{axis}")),
2366 )
2367 }
2368 ProceduralSurfaceDefinition::ParallelOffset {
2369 support,
2370 distance,
2371 self_intersect,
2372 } => {
2373 let support = self.emit_surface(support.as_str())?;
2374 Some(self.emitter.emit(
2375 "OFFSET_SURFACE",
2376 &format!(
2377 "'',{support},{},{}",
2378 real(*distance),
2379 logical(*self_intersect)
2380 ),
2381 ))
2382 }
2383 ProceduralSurfaceDefinition::Subset {
2384 support,
2385 parameter_ranges,
2386 u_sense: Some(u_sense),
2387 v_sense: Some(v_sense),
2388 } => {
2389 let support = self.emit_surface(support.as_str())?;
2390 Some(self.emitter.emit(
2391 "RECTANGULAR_TRIMMED_SURFACE",
2392 &format!(
2393 "'',{support},{},{},{},{},{},{}",
2394 real(parameter_ranges[0][0]),
2395 real(parameter_ranges[0][1]),
2396 real(parameter_ranges[1][0]),
2397 real(parameter_ranges[1][1]),
2398 if *u_sense { ".T." } else { ".F." },
2399 if *v_sense { ".T." } else { ".F." },
2400 ),
2401 ))
2402 }
2403 ProceduralSurfaceDefinition::Replica { source, transform } => {
2404 let source = self.emit_surface(source.as_str())?;
2405 let operator = geometry::transformation_operator(&mut self.emitter, *transform);
2406 Some(
2407 self.emitter
2408 .emit("SURFACE_REPLICA", &format!("'',{source},{operator}")),
2409 )
2410 }
2411 ProceduralSurfaceDefinition::DegenerateTorus { select_outer } => {
2412 let SurfaceGeometry::Torus {
2413 center,
2414 axis,
2415 ref_direction,
2416 major_radius,
2417 minor_radius,
2418 } = solved
2419 else {
2420 return None;
2421 };
2422 let placement =
2423 geometry::placement(&mut self.emitter, *center, *axis, *ref_direction);
2424 Some(self.emitter.emit(
2425 "DEGENERATE_TOROIDAL_SURFACE",
2426 &format!(
2427 "'',{placement},{},{},{}",
2428 real(major_radius.abs()),
2429 real(minor_radius.abs()),
2430 if *select_outer { ".T." } else { ".F." }
2431 ),
2432 ))
2433 }
2434 _ => None,
2435 }
2436 }
2437
2438 pub(crate) fn emit_curve(&mut self, curve_id: &str) -> Option<Ref> {
2439 if let Some(r) = self.curve_refs.get(curve_id) {
2440 return Some(*r);
2441 }
2442 if self.geometry_emission_depth >= 256 || !self.active_curves.insert(curve_id.to_string()) {
2443 return None;
2444 }
2445 self.geometry_emission_depth += 1;
2446 let result = (|| {
2447 let geometry = self.curves.get(curve_id)?.geometry.clone();
2448 let procedural = self
2449 .procedural_curves
2450 .get(curve_id)
2451 .map(|procedural| (procedural.id.0.clone(), procedural.definition.clone()));
2452 let emitted = procedural.and_then(|(id, definition)| {
2453 self.emit_procedural_curve(&definition)
2454 .map(|reference| (id, reference))
2455 });
2456 let r = if let Some((id, reference)) = emitted {
2457 self.written_procedural_curves.insert(id);
2458 reference
2459 } else if let CurveGeometry::Composite {
2460 segments,
2461 self_intersect,
2462 } = &geometry
2463 {
2464 let mut segment_refs = Vec::with_capacity(segments.len());
2465 for segment in segments {
2466 let curve = self.emit_curve(segment.curve.as_str())?;
2467 let transition = match segment.transition {
2468 cadmpeg_ir::geometry::CompositeCurveTransition::Discontinuous => {
2469 ".DISCONTINUOUS."
2470 }
2471 cadmpeg_ir::geometry::CompositeCurveTransition::Continuous => ".CONTINUOUS.",
2472 cadmpeg_ir::geometry::CompositeCurveTransition::ContSameGradient => {
2473 ".CONTSAMEGRADIENT."
2474 }
2475 cadmpeg_ir::geometry::CompositeCurveTransition::ContSameGradientSameCurvature => {
2476 ".CONTSAMEGRADIENTSAMECURVATURE."
2477 }
2478 };
2479 segment_refs.push(self.emitter.emit(
2480 "COMPOSITE_CURVE_SEGMENT",
2481 &format!(
2482 "{transition},{},{curve}",
2483 if segment.same_sense { ".T." } else { ".F." }
2484 ),
2485 ));
2486 }
2487 self.emitter.emit(
2488 "COMPOSITE_CURVE",
2489 &format!(
2490 "'',{},{}",
2491 refs(&segment_refs),
2492 match self_intersect {
2493 Some(true) => ".T.",
2494 Some(false) => ".F.",
2495 None => ".U.",
2496 }
2497 ),
2498 )
2499 } else if !geometry::curve_is_supported(&geometry) {
2500 return None;
2501 } else {
2502 geometry::curve(&mut self.emitter, &geometry)?
2503 };
2504 Some(r)
2505 })();
2506 self.active_curves.remove(curve_id);
2507 self.geometry_emission_depth -= 1;
2508 if let Some(r) = result {
2509 self.curve_refs.insert(curve_id.to_string(), r);
2510 }
2511 result
2512 }
2513
2514 fn emit_procedural_curve(&mut self, definition: &ProceduralCurveDefinition) -> Option<Ref> {
2515 match definition {
2516 ProceduralCurveDefinition::Subset {
2517 source,
2518 parameter_range: [start, end],
2519 sense,
2520 } => {
2521 let source = self.emit_curve(source.as_str())?;
2522 let (start, end) = if *sense {
2523 (*start, *end)
2524 } else {
2525 (*end, *start)
2526 };
2527 Some(self.emitter.emit(
2528 "TRIMMED_CURVE",
2529 &format!(
2530 "'',{source},(PARAMETER_VALUE({})),(PARAMETER_VALUE({})),{},.PARAMETER.",
2531 real(start),
2532 real(end),
2533 if *sense { ".T." } else { ".F." }
2534 ),
2535 ))
2536 }
2537 ProceduralCurveDefinition::Replica { source, transform } => {
2538 let source = self.emit_curve(source.as_str())?;
2539 let operator = geometry::transformation_operator(&mut self.emitter, *transform);
2540 Some(
2541 self.emitter
2542 .emit("CURVE_REPLICA", &format!("'',{source},{operator}")),
2543 )
2544 }
2545 ProceduralCurveDefinition::SpatialOffset {
2546 source,
2547 distance,
2548 reference_direction,
2549 self_intersect,
2550 } => {
2551 let source = self.emit_curve(source.as_str())?;
2552 let direction = geometry::direction(&mut self.emitter, *reference_direction);
2553 let self_intersect = match self_intersect {
2554 Some(true) => ".T.",
2555 Some(false) => ".F.",
2556 None => ".U.",
2557 };
2558 Some(self.emitter.emit(
2559 "OFFSET_CURVE_3D",
2560 &format!(
2561 "'',{source},{},{self_intersect},{direction}",
2562 real(*distance)
2563 ),
2564 ))
2565 }
2566 _ => None,
2567 }
2568 }
2569
2570 fn emit_pmi(&mut self, context: Ref) {
2571 if self.ir.model.pmi.is_empty() || !self.schema.supports_semantic_pmi() {
2572 return;
2573 }
2574 let annotations = self.ir.model.pmi.clone();
2575 let Some(pds) = self.default_product_definition_shape else {
2576 return;
2577 };
2578 let mut annotation_refs = HashMap::new();
2579 let mut aspects = HashMap::<String, Ref>::new();
2580 for annotation in &annotations {
2581 for target in &annotation.targets {
2582 let PmiTarget::ShapeAspect { source_id } = target else {
2583 continue;
2584 };
2585 aspects.entry(source_id.clone()).or_insert_with(|| {
2586 self.emitter.emit(
2587 "SHAPE_ASPECT",
2588 &format!("{},'',{pds},.T.", string(source_id)),
2589 )
2590 });
2591 }
2592 }
2593 let fallback_aspect = self
2594 .emitter
2595 .emit("SHAPE_ASPECT", &format!("'PMI target','',{pds},.T."));
2596 let target_ref = |annotation: &cadmpeg_ir::PmiAnnotation| {
2597 annotation.targets.iter().find_map(|target| {
2598 if let PmiTarget::ShapeAspect { source_id } = target {
2599 aspects.get(source_id).copied()
2600 } else {
2601 None
2602 }
2603 })
2604 };
2605 let targets_exact = |annotation: &cadmpeg_ir::PmiAnnotation| {
2606 annotation
2607 .targets
2608 .iter()
2609 .all(|target| matches!(target, PmiTarget::ShapeAspect { .. }))
2610 };
2611
2612 for annotation in &annotations {
2613 if let PmiDefinition::Datum { identification } = &annotation.definition {
2614 let datum = self.emitter.emit(
2615 "DATUM",
2616 &format!(
2617 "{},$,{pds},.F.,{}",
2618 string(annotation.name.as_deref().unwrap_or("")),
2619 string(identification)
2620 ),
2621 );
2622 annotation_refs.insert(annotation.id.clone(), datum);
2623 self.written_pmi += usize::from(targets_exact(annotation));
2624 }
2625 }
2626 for annotation in &annotations {
2627 if let PmiDefinition::DatumSystem { references } = &annotation.definition {
2628 let mut groups = BTreeMap::<(u32, Option<u32>), Vec<_>>::new();
2629 for reference in references {
2630 groups
2631 .entry((reference.precedence, reference.common_group))
2632 .or_default()
2633 .push(reference);
2634 }
2635 let compartments = groups
2636 .values()
2637 .filter_map(|group| {
2638 let datum_refs = group
2639 .iter()
2640 .map(|reference| annotation_refs.get(&reference.datum).copied())
2641 .collect::<Option<Vec<_>>>()?;
2642 if group[0].common_group.is_none() && group.len() != 1 {
2643 return None;
2644 }
2645 let (datum, modifiers) = if group[0].common_group.is_some() {
2646 let elements = group
2647 .iter()
2648 .zip(datum_refs)
2649 .map(|(reference, datum)| {
2650 let modifiers =
2651 self.emit_datum_modifiers(&reference.modifiers)?;
2652 Some(self.emitter.emit(
2653 "DATUM_REFERENCE_ELEMENT",
2654 &format!("'',$,{pds},.F.,{datum},({modifiers})"),
2655 ))
2656 })
2657 .collect::<Option<Vec<_>>>()?;
2658 (
2659 format!("COMMON_DATUM_LIST({})", refs(&elements)),
2660 String::new(),
2661 )
2662 } else {
2663 (
2664 datum_refs[0].to_string(),
2665 self.emit_datum_modifiers(&group[0].modifiers)?,
2666 )
2667 };
2668 Some(self.emitter.emit(
2669 "DATUM_REFERENCE_COMPARTMENT",
2670 &format!("'',$,{pds},.F.,{datum},({modifiers})"),
2671 ))
2672 })
2673 .collect::<Vec<_>>();
2674 let complete = compartments.len() == groups.len();
2675 if compartments.is_empty() {
2676 continue;
2677 }
2678 let system = self.emitter.emit(
2679 "DATUM_SYSTEM",
2680 &format!(
2681 "{},'',{pds},.F.,{}",
2682 string(annotation.name.as_deref().unwrap_or("")),
2683 refs(&compartments)
2684 ),
2685 );
2686 annotation_refs.insert(annotation.id.clone(), system);
2687 self.written_pmi += usize::from(targets_exact(annotation) && complete);
2688 }
2689 }
2690 for annotation in &annotations {
2691 match &annotation.definition {
2692 PmiDefinition::Dimension {
2693 dimension,
2694 nominal,
2695 lower_deviation,
2696 upper_deviation,
2697 limits_and_fits,
2698 } => {
2699 let aspect = target_ref(annotation).unwrap_or(fallback_aspect);
2700 let name = annotation.name.as_deref().unwrap_or("");
2701 let (entity, kind_exact) = match dimension {
2702 DimensionKind::Size => ("DIMENSIONAL_SIZE", true),
2703 DimensionKind::Location => ("DIMENSIONAL_LOCATION", true),
2704 DimensionKind::Angular => ("ANGULAR_SIZE", true),
2705 DimensionKind::Diameter | DimensionKind::Radius => {
2710 ("DIMENSIONAL_SIZE", true)
2711 }
2712 DimensionKind::Other(_) => ("DIMENSIONAL_SIZE", false),
2713 };
2714 let characteristic_name = match dimension {
2715 DimensionKind::Diameter => "diameter",
2716 DimensionKind::Radius => "radius",
2717 _ => name,
2718 };
2719 let parameters = match dimension {
2720 DimensionKind::Location => {
2721 format!("{},$,{aspect},{aspect}", string(characteristic_name))
2722 }
2723 DimensionKind::Angular => {
2724 format!("{aspect},{},.SMALL.", string(characteristic_name))
2725 }
2726 _ => format!("{aspect},{}", string(characteristic_name)),
2727 };
2728 let characteristic = self.emitter.emit(entity, ¶meters);
2729 if let Some(value) = nominal {
2730 let measure = self.emit_pmi_measure_representation_item(*value, name);
2731 let representation = self.emitter.emit(
2732 "SHAPE_DIMENSION_REPRESENTATION",
2733 &format!("'',({measure}),{context}"),
2734 );
2735 self.emitter.emit(
2736 "DIMENSIONAL_CHARACTERISTIC_REPRESENTATION",
2737 &format!("{characteristic},{representation}"),
2738 );
2739 }
2740 if let (Some(lower), Some(upper)) = (lower_deviation, upper_deviation) {
2741 let lower = self.emit_pmi_measure(*lower);
2742 let upper = self.emit_pmi_measure(*upper);
2743 let tolerance = self
2744 .emitter
2745 .emit("TOLERANCE_VALUE", &format!("{lower},{upper}"));
2746 self.emitter.emit(
2747 "PLUS_MINUS_TOLERANCE",
2748 &format!("{tolerance},{characteristic}"),
2749 );
2750 }
2751 if let Some(fit) = limits_and_fits {
2752 let fit = self.emitter.emit(
2753 "LIMITS_AND_FITS",
2754 &format!(
2755 "{},{},{},{}",
2756 string(&fit.form_variance),
2757 string(&fit.zone_variance),
2758 string(&fit.grade),
2759 string(&fit.source)
2760 ),
2761 );
2762 self.emitter
2763 .emit("PLUS_MINUS_TOLERANCE", &format!("{fit},{characteristic}"));
2764 }
2765 annotation_refs.insert(annotation.id.clone(), characteristic);
2766 let deviations_exact = lower_deviation.is_some() == upper_deviation.is_some();
2767 self.written_pmi +=
2768 usize::from(targets_exact(annotation) && deviations_exact && kind_exact);
2769 }
2770 PmiDefinition::GeometricTolerance {
2771 tolerance,
2772 magnitude,
2773 defined_unit,
2774 defined_area_unit,
2775 defined_area_second_unit,
2776 datum_system,
2777 modifiers,
2778 } => {
2779 let kind_exact = !matches!(tolerance, GeometricToleranceKind::Other(value) if value != "geometric_tolerance");
2780 let entity = match tolerance {
2781 GeometricToleranceKind::Straightness => "STRAIGHTNESS_TOLERANCE",
2782 GeometricToleranceKind::Flatness => "FLATNESS_TOLERANCE",
2783 GeometricToleranceKind::Roundness => "ROUNDNESS_TOLERANCE",
2784 GeometricToleranceKind::Cylindricity => "CYLINDRICITY_TOLERANCE",
2785 GeometricToleranceKind::Coaxiality => "COAXIALITY_TOLERANCE",
2786 GeometricToleranceKind::LineProfile => "LINE_PROFILE_TOLERANCE",
2787 GeometricToleranceKind::SurfaceProfile => "SURFACE_PROFILE_TOLERANCE",
2788 GeometricToleranceKind::Angularity => "ANGULARITY_TOLERANCE",
2789 GeometricToleranceKind::Perpendicularity => "PERPENDICULARITY_TOLERANCE",
2790 GeometricToleranceKind::Parallelism => "PARALLELISM_TOLERANCE",
2791 GeometricToleranceKind::Position => "POSITION_TOLERANCE",
2792 GeometricToleranceKind::Concentricity => "CONCENTRICITY_TOLERANCE",
2793 GeometricToleranceKind::Symmetry => "SYMMETRY_TOLERANCE",
2794 GeometricToleranceKind::CircularRunout => "CIRCULAR_RUNOUT_TOLERANCE",
2795 GeometricToleranceKind::TotalRunout => "TOTAL_RUNOUT_TOLERANCE",
2796 GeometricToleranceKind::Other(_) => continue,
2797 };
2798 let aspect = target_ref(annotation).unwrap_or(fallback_aspect);
2799 let modifier_values = Self::emit_geometric_tolerance_modifiers(modifiers);
2800 if !modifiers.is_empty() && modifier_values.is_none() {
2801 continue;
2802 }
2803 let area_unit =
2804 Self::emit_geometric_tolerance_area_unit(defined_area_unit.as_deref());
2805 if defined_area_unit.is_some() && area_unit.is_none()
2806 || defined_area_second_unit.is_some() && area_unit.is_none()
2807 {
2808 continue;
2809 }
2810 let datum_ref = datum_system
2811 .as_ref()
2812 .and_then(|id| annotation_refs.get(id).copied());
2813 if datum_system.is_some() && datum_ref.is_none() {
2814 continue;
2815 }
2816 let measure = self.emit_pmi_measure(*magnitude);
2817 let tolerance_ref = if datum_ref.is_none()
2818 && modifiers.is_empty()
2819 && defined_unit.is_none()
2820 && area_unit.is_none()
2821 {
2822 self.emitter.emit(
2823 entity,
2824 &format!(
2825 "{},'',{measure},{aspect}",
2826 string(annotation.name.as_deref().unwrap_or(""))
2827 ),
2828 )
2829 } else {
2830 let mut parts = vec![format!(
2831 "GEOMETRIC_TOLERANCE({},{},{measure},{aspect})",
2832 string(annotation.name.as_deref().unwrap_or("")),
2833 "''"
2834 )];
2835 if area_unit.is_some() || defined_unit.is_some() {
2836 let defined_unit = defined_unit.as_ref().map_or_else(
2837 || "$".into(),
2838 |unit| self.emit_pmi_measure(*unit).to_string(),
2839 );
2840 parts.push(format!(
2841 "GEOMETRIC_TOLERANCE_WITH_DEFINED_UNIT({defined_unit})"
2842 ));
2843 }
2844 if let Some(area_unit) = area_unit {
2845 let second_unit = defined_area_second_unit.as_ref().map_or_else(
2846 || "$".into(),
2847 |unit| self.emit_pmi_measure(*unit).to_string(),
2848 );
2849 parts.push(format!(
2850 "GEOMETRIC_TOLERANCE_WITH_DEFINED_AREA_UNIT(.{area_unit}.,{second_unit})"
2851 ));
2852 }
2853 if let Some(datum) = datum_ref {
2854 parts.push(format!(
2855 "GEOMETRIC_TOLERANCE_WITH_DATUM_REFERENCE(({datum}))"
2856 ));
2857 }
2858 if let Some(modifiers) = modifier_values {
2859 parts
2860 .push(format!("GEOMETRIC_TOLERANCE_WITH_MODIFIERS(({modifiers}))"));
2861 }
2862 parts.push(format!("{entity}()"));
2863 self.emitter
2864 .emit_raw("GEOMETRIC_TOLERANCE", &format!("({})", parts.join(" ")))
2865 };
2866 annotation_refs.insert(annotation.id.clone(), tolerance_ref);
2867 self.written_pmi += usize::from(targets_exact(annotation) && kind_exact);
2868 }
2869 PmiDefinition::Datum { .. }
2870 | PmiDefinition::DatumSystem { .. }
2871 | PmiDefinition::Presentation { .. } => {}
2872 }
2873 }
2874 let mut presentation_items = Vec::new();
2875 let mut presentation_semantics = Vec::new();
2876 for annotation in &annotations {
2877 let PmiDefinition::Presentation {
2878 text,
2879 placement,
2880 semantics,
2881 } = &annotation.definition
2882 else {
2883 continue;
2884 };
2885 let (Some(text), Some(placement)) = (text.as_deref(), placement.as_ref()) else {
2886 continue;
2887 };
2888 if !annotation.targets.is_empty() || !is_rigid_transform(&placement.rows) {
2889 continue;
2890 }
2891 let rows = placement.rows;
2892 let placement = geometry::placement(
2893 &mut self.emitter,
2894 cadmpeg_ir::math::Point3::new(rows[0][3], rows[1][3], rows[2][3]),
2895 cadmpeg_ir::math::Vector3::new(rows[0][2], rows[1][2], rows[2][2]),
2896 cadmpeg_ir::math::Vector3::new(rows[0][0], rows[1][0], rows[2][0]),
2897 );
2898 let font_source = self.emitter.emit("EXTERNAL_SOURCE", "'ISO 3098'");
2899 let font = self.emitter.emit(
2900 "EXTERNALLY_DEFINED_TEXT_FONT",
2901 &format!("IDENTIFIER('ISO 3098'),{font_source}"),
2902 );
2903 let literal = self.emitter.emit(
2904 "TEXT_LITERAL",
2905 &format!("{},{placement},'left',.RIGHT.,{font}", string(text)),
2906 );
2907 let semantic_refs = semantics
2908 .iter()
2909 .filter_map(|semantic| annotation_refs.get(semantic).copied())
2910 .collect::<Vec<_>>();
2911 if semantic_refs.len() != semantics.len() {
2912 continue;
2913 }
2914 let style = self
2915 .emitter
2916 .emit("PRESENTATION_STYLE_ASSIGNMENT", "(.NULL.)");
2917 let occurrence = self.emitter.emit(
2918 "ANNOTATION_TEXT_OCCURRENCE",
2919 &format!(
2920 "{},{},{literal}",
2921 string(annotation.name.as_deref().unwrap_or("")),
2922 refs(&[style])
2923 ),
2924 );
2925 presentation_items.push(occurrence);
2926 presentation_semantics.push((occurrence, semantic_refs));
2927 annotation_refs.insert(annotation.id.clone(), occurrence);
2928 self.written_pmi += 1;
2929 }
2930 if !presentation_items.is_empty() {
2931 let model = self.emitter.emit(
2932 "DRAUGHTING_MODEL",
2933 &format!(
2934 "'PMI presentation',{}, {context}",
2935 refs(&presentation_items)
2936 ),
2937 );
2938 for (occurrence, semantics) in presentation_semantics {
2939 for semantic in semantics {
2940 self.emitter.emit(
2941 "DRAUGHTING_MODEL_ITEM_ASSOCIATION",
2942 &format!("'','',{semantic},{model},{occurrence}"),
2943 );
2944 }
2945 }
2946 }
2947 for (annotation, reference) in annotation_refs {
2948 self.pmi_step_refs.insert(annotation.0, reference);
2949 }
2950 }
2951
2952 fn emit_datum_modifiers(&mut self, source: &[String]) -> Option<String> {
2953 const SIMPLE: &[&str] = &[
2954 "free_state",
2955 "basic",
2956 "translation",
2957 "least_material_requirement",
2958 "maximum_material_requirement",
2959 "point",
2960 "line",
2961 "plane",
2962 "orientation",
2963 "any_cross_section",
2964 "any_longitudinal_section",
2965 "contacting_feature",
2966 "distance_variable",
2967 "degree_of_freedom_constraint_x",
2968 "degree_of_freedom_constraint_y",
2969 "degree_of_freedom_constraint_z",
2970 "degree_of_freedom_constraint_u",
2971 "degree_of_freedom_constraint_v",
2972 "degree_of_freedom_constraint_w",
2973 "minor_diameter",
2974 "major_diameter",
2975 "pitch_diameter",
2976 ];
2977 const WITH_VALUE: &[&str] = &[
2978 "circular_or_cylindrical",
2979 "spherical",
2980 "distance",
2981 "projected",
2982 ];
2983 enum Modifier {
2984 Simple(String),
2985 WithValue { kind: String, value: f64 },
2986 }
2987 let parsed = source
2988 .iter()
2989 .map(|modifier| {
2990 if let Some((kind, value)) = modifier.split_once(':') {
2991 let kind = kind.to_ascii_lowercase();
2992 let value = value.parse::<f64>().ok()?;
2993 if !value.is_finite() {
2994 return None;
2995 }
2996 WITH_VALUE
2997 .contains(&kind.as_str())
2998 .then_some(Modifier::WithValue { kind, value })
2999 } else {
3000 let modifier = modifier.to_ascii_lowercase();
3001 SIMPLE
3002 .contains(&modifier.as_str())
3003 .then_some(Modifier::Simple(modifier))
3004 }
3005 })
3006 .collect::<Option<Vec<_>>>()?;
3007 let mut modifiers = Vec::with_capacity(source.len());
3008 for modifier in parsed {
3009 match modifier {
3010 Modifier::WithValue { kind, value } => {
3011 let measure = self.emit_pmi_measure(cadmpeg_ir::PmiValue {
3012 value,
3013 quantity: cadmpeg_ir::PmiQuantity::Length,
3014 });
3015 modifiers.push(
3016 self.emitter
3017 .emit(
3018 "DATUM_REFERENCE_MODIFIER_WITH_VALUE",
3019 &format!(".{}.,{measure}", kind.to_ascii_uppercase()),
3020 )
3021 .to_string(),
3022 );
3023 }
3024 Modifier::Simple(modifier) => {
3025 modifiers.push(format!(".{}.", modifier.to_ascii_uppercase()));
3026 }
3027 }
3028 }
3029 Some(modifiers.join(","))
3030 }
3031
3032 fn emit_geometric_tolerance_modifiers(source: &[String]) -> Option<String> {
3033 const SUPPORTED: &[&str] = &[
3034 "any_cross_section",
3035 "associated_least_square_feature",
3036 "associated_maximum_inscribed_feature",
3037 "associated_minimum_inscribed_feature",
3038 "associated_minmax_feature",
3039 "associated_tangent_feature",
3040 "circle_a",
3041 "common_zone",
3042 "continuous_features",
3043 "derived_feature",
3044 "each_element",
3045 "each_radial_element",
3046 "free_state",
3047 "individually",
3048 "least_material_requirement",
3049 "line_element",
3050 "major_diameter",
3051 "maximum_material_requirement",
3052 "minor_diameter",
3053 "not_convex",
3054 "offset_zone",
3055 "peak_height",
3056 "pitch_diameter",
3057 "reciprocity_requirement",
3058 "reference_least_square_feature_with_external_material_constraint",
3059 "reference_least_square_feature_with_internal_material_constraint",
3060 "reference_least_square_feature_without_constraint",
3061 "reference_maximum_inscribed_feature",
3062 "reference_minimax_feature_with_external_material_constraint",
3063 "reference_minimax_feature_with_internal_material_constraint",
3064 "reference_minimax_feature_without_constraint",
3065 "reference_minimum_circumscribed_feature",
3066 "separate_requirement",
3067 "separate_zones",
3068 "standard_deviation",
3069 "statistical_tolerance",
3070 "stock",
3071 "tangent_plane",
3072 "total_range_deviations",
3073 "united_feature",
3074 "unspecified_angular_tolerance_zone_offset",
3075 "unspecified_linear_tolerance_zone_offset",
3076 "valley_depth",
3077 "variable_angle",
3078 ];
3079 source
3080 .iter()
3081 .map(|modifier| {
3082 let normalized = modifier.to_ascii_lowercase();
3083 SUPPORTED
3084 .contains(&normalized.as_str())
3085 .then(|| format!(".{}.", normalized.to_ascii_uppercase()))
3086 })
3087 .collect::<Option<Vec<_>>>()
3088 .map(|modifiers| modifiers.join(","))
3089 }
3090
3091 fn emit_geometric_tolerance_area_unit(source: Option<&str>) -> Option<String> {
3092 let source = source?;
3093 matches!(
3094 source.to_ascii_lowercase().as_str(),
3095 "circular" | "square" | "rectangular" | "cylindrical" | "spherical"
3096 )
3097 .then(|| source.to_ascii_lowercase())
3098 }
3099
3100 fn emit_pmi_measure(&mut self, value: cadmpeg_ir::PmiValue) -> Ref {
3101 let (entity, typed, unit) = match value.quantity {
3102 PmiQuantity::Length => (
3103 "LENGTH_MEASURE_WITH_UNIT",
3104 "LENGTH_MEASURE",
3105 self.emit_length_unit(),
3106 ),
3107 PmiQuantity::Angle => (
3108 "PLANE_ANGLE_MEASURE_WITH_UNIT",
3109 "PLANE_ANGLE_MEASURE",
3110 self.emit_angle_unit(),
3111 ),
3112 PmiQuantity::Ratio => ("MEASURE_WITH_UNIT", "RATIO_MEASURE", self.emit_ratio_unit()),
3113 };
3114 self.emitter
3115 .emit(entity, &format!("{typed}({}),{unit}", real(value.value)))
3116 }
3117
3118 fn emit_pmi_measure_representation_item(
3119 &mut self,
3120 value: cadmpeg_ir::PmiValue,
3121 name: &str,
3122 ) -> Ref {
3123 let (typed, unit) = match value.quantity {
3124 PmiQuantity::Length => ("LENGTH_MEASURE", self.emit_length_unit()),
3125 PmiQuantity::Angle => ("PLANE_ANGLE_MEASURE", self.emit_angle_unit()),
3126 PmiQuantity::Ratio => ("RATIO_MEASURE", self.emit_ratio_unit()),
3127 };
3128 self.emitter.emit(
3129 "MEASURE_REPRESENTATION_ITEM",
3130 &format!("{},{typed}({}),{unit}", string(name), real(value.value)),
3131 )
3132 }
3133
3134 fn note_unrepresented(&mut self) {
3135 let mut referenced_shells = BTreeSet::new();
3136 let mut referenced_faces = BTreeSet::new();
3137 let mut referenced_loops = BTreeSet::new();
3138 let mut referenced_coedges = BTreeSet::new();
3139 let mut referenced_edges = BTreeSet::new();
3140 let mut referenced_vertices = BTreeSet::new();
3141 for region in &self.ir.model.regions {
3142 for shell_id in ®ion.shells {
3143 if !referenced_shells.insert(shell_id.as_str()) {
3144 continue;
3145 }
3146 let Some(shell) = self.shells.get(shell_id.as_str()).copied() else {
3147 continue;
3148 };
3149 for edge_id in &shell.wire_edges {
3150 referenced_edges.insert(edge_id.as_str());
3151 if let Some(edge) = self.edges.get(edge_id.as_str()).copied() {
3152 referenced_vertices.insert(edge.start.as_str());
3153 referenced_vertices.insert(edge.end.as_str());
3154 }
3155 }
3156 referenced_vertices.extend(
3157 shell
3158 .free_vertices
3159 .iter()
3160 .map(cadmpeg_ir::ids::VertexId::as_str),
3161 );
3162 for face_id in &shell.faces {
3163 if !referenced_faces.insert(face_id.as_str()) {
3164 continue;
3165 }
3166 let Some(face) = self.faces.get(face_id.as_str()).copied() else {
3167 continue;
3168 };
3169 for loop_id in &face.loops {
3170 if !referenced_loops.insert(loop_id.as_str()) {
3171 continue;
3172 }
3173 let Some(loop_) = self.loops.get(loop_id.as_str()).copied() else {
3174 continue;
3175 };
3176 referenced_vertices
3177 .extend(loop_.vertex_uses.iter().map(|use_| use_.vertex.as_str()));
3178 for vertex_use in &loop_.vertex_uses {
3179 if let Some(after) = &vertex_use.after {
3180 referenced_coedges.insert(after.as_str());
3181 }
3182 }
3183 for coedge_id in &loop_.coedges {
3184 if !referenced_coedges.insert(coedge_id.as_str()) {
3185 continue;
3186 }
3187 let Some(coedge) = self.coedges.get(coedge_id.as_str()).copied() else {
3188 continue;
3189 };
3190 referenced_edges.insert(coedge.edge.as_str());
3191 if let Some(edge) = self.edges.get(coedge.edge.as_str()).copied() {
3192 referenced_vertices.insert(edge.start.as_str());
3193 referenced_vertices.insert(edge.end.as_str());
3194 }
3195 }
3196 }
3197 }
3198 }
3199 }
3200 let omitted_bodies = self
3201 .ir
3202 .model
3203 .bodies
3204 .iter()
3205 .filter(|body| !self.body_shape_refs.contains_key(body.id.as_str()))
3206 .count();
3207 let omitted_shells = self
3208 .ir
3209 .model
3210 .shells
3211 .iter()
3212 .filter(|shell| !referenced_shells.contains(shell.id.as_str()))
3213 .count();
3214 let omitted_faces = self
3215 .ir
3216 .model
3217 .faces
3218 .iter()
3219 .filter(|face| !referenced_faces.contains(face.id.as_str()))
3220 .count();
3221 let omitted_loops = self
3222 .ir
3223 .model
3224 .loops
3225 .iter()
3226 .filter(|loop_| !referenced_loops.contains(loop_.id.as_str()))
3227 .count();
3228 let omitted_coedges = self
3229 .ir
3230 .model
3231 .coedges
3232 .iter()
3233 .filter(|coedge| !referenced_coedges.contains(coedge.id.as_str()))
3234 .count();
3235 let omitted_edges = self
3236 .ir
3237 .model
3238 .edges
3239 .iter()
3240 .filter(|edge| !referenced_edges.contains(edge.id.as_str()))
3241 .count();
3242 let omitted_vertices = self
3243 .ir
3244 .model
3245 .vertices
3246 .iter()
3247 .filter(|vertex| !referenced_vertices.contains(vertex.id.as_str()))
3248 .count();
3249 let omitted_topology = [
3250 ("body", omitted_bodies),
3251 ("shell", omitted_shells),
3252 ("face", omitted_faces),
3253 ("loop", omitted_loops),
3254 ("coedge", omitted_coedges),
3255 ("edge", omitted_edges),
3256 ("vertex", omitted_vertices),
3257 ];
3258 if omitted_topology.iter().any(|(_, count)| *count > 0) {
3259 let details = omitted_topology
3260 .iter()
3261 .filter(|(_, count)| *count > 0)
3262 .map(|(kind, count)| format!("{count} {kind}(s)"))
3263 .collect::<Vec<_>>()
3264 .join(", ");
3265 self.loss(
3266 StepLossCode::TopologyUnreachableFromRegion,
3267 format!("topology not reachable from any emitted region shape item: {details}"),
3268 );
3269 }
3270 let nonstandard_analytic_surfaces = self
3271 .ir
3272 .model
3273 .surfaces
3274 .iter()
3275 .filter(|surface| match &surface.geometry {
3276 SurfaceGeometry::Sphere { radius, .. } => *radius < 0.0,
3277 SurfaceGeometry::Torus {
3278 major_radius,
3279 minor_radius,
3280 ..
3281 } => {
3282 *major_radius < 0.0
3283 || *minor_radius < 0.0
3284 || (minor_radius.abs() > major_radius.abs()
3285 && !self.ir.model.procedural_surfaces.iter().any(|procedural| {
3286 procedural.surface == surface.id
3287 && self.written_procedural_surfaces.contains(&procedural.id.0)
3288 && matches!(
3289 procedural.definition,
3290 ProceduralSurfaceDefinition::DegenerateTorus { .. }
3291 )
3292 }))
3293 }
3294 _ => false,
3295 })
3296 .count();
3297 if nonstandard_analytic_surfaces > 0 {
3298 self.loss(
3299 StepLossCode::AnalyticSurfaceNormalized,
3300 format!(
3301 "{nonstandard_analytic_surfaces} signed or self-intersecting analytic \
3302 surface(s) were normalized to positive STEP radii"
3303 ),
3304 );
3305 }
3306 let elliptical_cones = self
3307 .ir
3308 .model
3309 .surfaces
3310 .iter()
3311 .filter(|surface| {
3312 matches!(
3313 surface.geometry,
3314 SurfaceGeometry::Cone { ratio, .. } if ratio != 1.0
3315 )
3316 })
3317 .count();
3318 if elliptical_cones > 0 {
3319 self.loss(
3320 StepLossCode::EllipticalConeReduced,
3321 format!(
3322 "{elliptical_cones} elliptical cone surface(s) were reduced to circular STEP CONICAL_SURFACE carriers"
3323 ),
3324 );
3325 }
3326 if !self.curveless_edges.is_empty() {
3327 self.loss(
3328 StepLossCode::CurvelessEdgeOmitted,
3329 format!(
3330 "{} edge(s) have no typed 3D curve or carry a STEP-unsupported transform and were omitted from \
3331 their edge loops (STEP EDGE_CURVE requires a 3D curve)",
3332 self.curveless_edges.len()
3333 ),
3334 );
3335 }
3336 if !self.unknown_surface_faces.is_empty() {
3337 self.loss(
3338 StepLossCode::UnknownSurfaceFaceOmitted,
3339 format!(
3340 "{} face(s) rest on an unknown or STEP-unsupported surface and were omitted \
3341 from the STEP shell (an ADVANCED_FACE requires a surface); their \
3342 topology remains in the IR",
3343 self.unknown_surface_faces.len()
3344 ),
3345 );
3346 }
3347 if !self.unwritten_geometry_carriers.is_empty() {
3348 let carriers = self
3349 .unwritten_geometry_carriers
3350 .iter()
3351 .map(|id| format!("'{id}'"))
3352 .collect::<Vec<_>>()
3353 .join(", ");
3354 self.loss(
3355 StepLossCode::GeometryCarrierNotWritten,
3356 format!(
3357 "{} geometry carrier(s) were not written: {carriers}",
3358 self.unwritten_geometry_carriers.len()
3359 ),
3360 );
3361 }
3362 if !self.unwritten_pcurve_carriers.is_empty() {
3363 let pcurves = self
3364 .unwritten_pcurve_carriers
3365 .iter()
3366 .map(|id| format!("'{id}'"))
3367 .collect::<Vec<_>>()
3368 .join(", ");
3369 self.loss(
3370 StepLossCode::PcurveCarrierUnwritable,
3371 format!(
3372 "{} coedge pcurve carrier(s) use geometry or surface references that were not writable: {pcurves}",
3373 self.unwritten_pcurve_carriers.len()
3374 ),
3375 );
3376 }
3377 if !self.missing_parent_products.is_empty() {
3378 let occurrences = self
3379 .missing_parent_products
3380 .iter()
3381 .map(|id| format!("'{id}'"))
3382 .collect::<Vec<_>>()
3383 .join(", ");
3384 self.loss(
3385 StepLossCode::AssemblyOccurrenceOmittedNoParentProduct,
3386 format!(
3387 "{} assembly occurrence(s) were omitted because their parent has no local product definition: {occurrences}",
3388 self.missing_parent_products.len()
3389 ),
3390 );
3391 }
3392 if !self.empty_regions.is_empty() {
3393 let regions = self
3394 .empty_regions
3395 .iter()
3396 .map(|id| format!("'{id}'"))
3397 .collect::<Vec<_>>()
3398 .join(", ");
3399 self.loss(
3400 StepLossCode::RegionNoShellList,
3401 format!(
3402 "{} region(s) have no shell list and were not written to STEP: {regions}",
3403 self.empty_regions.len()
3404 ),
3405 );
3406 }
3407 if !self.empty_wire_regions.is_empty() {
3408 let regions = self
3409 .empty_wire_regions
3410 .iter()
3411 .map(|id| format!("'{id}'"))
3412 .collect::<Vec<_>>()
3413 .join(", ");
3414 self.loss(
3415 StepLossCode::WireRegionNoConnectedEdgeSet,
3416 format!(
3417 "{} wire region(s) had no writable connected edge set and were not written to STEP: {regions}",
3418 self.empty_wire_regions.len()
3419 ),
3420 );
3421 }
3422 if !self.missing_wire_shells.is_empty() {
3423 let shells = self
3424 .missing_wire_shells
3425 .iter()
3426 .map(|(region, shell)| format!("'{region}' -> '{shell}'"))
3427 .collect::<Vec<_>>()
3428 .join(", ");
3429 self.loss(
3430 StepLossCode::WireRegionMissingShell,
3431 format!(
3432 "{} wire region/shell relation(s) referenced missing shell records: {shells}",
3433 self.missing_wire_shells.len()
3434 ),
3435 );
3436 }
3437 if !self.hidden_bodies_without_items.is_empty() {
3438 let bodies = self
3439 .hidden_bodies_without_items
3440 .iter()
3441 .map(|id| format!("'{id}'"))
3442 .collect::<Vec<_>>()
3443 .join(", ");
3444 self.loss(
3445 StepLossCode::HiddenBodyOmitted,
3446 format!(
3447 "{} hidden body/bodies had no emitted STEP item and were omitted from INVISIBILITY: {bodies}",
3448 self.hidden_bodies_without_items.len()
3449 ),
3450 );
3451 }
3452 if !self.dangling_appearance_bindings.is_empty() {
3453 let bindings = self
3454 .dangling_appearance_bindings
3455 .iter()
3456 .map(|(binding, appearance)| format!("'{binding}' -> '{appearance}'"))
3457 .collect::<Vec<_>>()
3458 .join(", ");
3459 self.loss(
3460 StepLossCode::AppearanceBindingMissingAsset,
3461 format!(
3462 "{} appearance binding(s) reference missing appearance assets and were not written: {bindings}",
3463 self.dangling_appearance_bindings.len()
3464 ),
3465 );
3466 }
3467 if !self.colorless_appearance_bindings.is_empty() {
3468 let bindings = self
3469 .colorless_appearance_bindings
3470 .iter()
3471 .map(|(binding, appearance)| format!("'{binding}' -> '{appearance}'"))
3472 .collect::<Vec<_>>()
3473 .join(", ");
3474 self.loss(
3475 StepLossCode::AppearanceBindingNoBaseColor,
3476 format!(
3477 "{} appearance binding(s) reference appearances without a base color and were not written: {bindings}",
3478 self.colorless_appearance_bindings.len()
3479 ),
3480 );
3481 }
3482 let missing_pcurve_count = self
3483 .ir
3484 .model
3485 .coedges
3486 .iter()
3487 .flat_map(|coedge| &coedge.pcurves)
3488 .filter(|use_| !self.pcurves.contains_key(use_.pcurve.as_str()))
3489 .count();
3490 if missing_pcurve_count > 0 {
3491 self.loss(
3492 StepLossCode::CoedgePcurveNoGeometry,
3493 format!(
3494 "{missing_pcurve_count} coedge pcurve reference(s) have no geometry and were not written"
3495 ),
3496 );
3497 }
3498 let reduced_pcurve_count = self
3499 .ir
3500 .model
3501 .coedges
3502 .iter()
3503 .flat_map(|coedge| &coedge.pcurves)
3504 .filter_map(|use_| self.pcurves.get(use_.pcurve.as_str()))
3505 .filter(|pcurve| {
3506 pcurve.wrapper_reversed.is_some()
3507 || pcurve.native_tail_flags.is_some()
3508 || pcurve.parameter_range.is_some()
3509 || pcurve.fit_tolerance.is_some()
3510 })
3511 .count();
3512 if reduced_pcurve_count > 0 {
3513 self.loss(
3514 StepLossCode::CoedgePcurveNativeMetadata,
3515 format!(
3516 "{reduced_pcurve_count} emitted coedge pcurve(s) carry native-only metadata not represented in STEP"
3517 ),
3518 );
3519 }
3520 let pcurve_use_metadata_count = self
3521 .ir
3522 .model
3523 .coedges
3524 .iter()
3525 .flat_map(|coedge| &coedge.pcurves)
3526 .chain(
3527 self.ir
3528 .model
3529 .loops
3530 .iter()
3531 .flat_map(|loop_| &loop_.vertex_uses)
3532 .flat_map(|vertex_use| &vertex_use.pcurves),
3533 )
3534 .filter(|use_| use_.isoparametric.is_some() || use_.parameter_range.is_some())
3535 .count();
3536 if pcurve_use_metadata_count > 0 {
3537 self.loss(
3538 StepLossCode::PcurveUseNativeMetadata,
3539 format!(
3540 "{pcurve_use_metadata_count} pcurve use(s) carry native-only parameter metadata not represented in STEP"
3541 ),
3542 );
3543 }
3544 let coedge_use_curve_metadata_count = self
3545 .ir
3546 .model
3547 .coedges
3548 .iter()
3549 .filter(|coedge| {
3550 coedge.use_curve.is_some() || coedge.use_curve_parameter_range.is_some()
3551 })
3552 .count();
3553 if coedge_use_curve_metadata_count > 0 {
3554 self.loss(
3555 StepLossCode::CoedgeUseCurveNotRepresented,
3556 format!(
3557 "{coedge_use_curve_metadata_count} coedge-local 3D curve use(s) were not represented in STEP"
3558 ),
3559 );
3560 }
3561 let topology_metadata = [
3565 (
3566 "face tolerance",
3567 self.ir
3568 .model
3569 .faces
3570 .iter()
3571 .filter(|face| face.tolerance.is_some())
3572 .count(),
3573 ),
3574 (
3575 "edge tolerance",
3576 self.ir
3577 .model
3578 .edges
3579 .iter()
3580 .filter(|edge| edge.tolerance.is_some())
3581 .count(),
3582 ),
3583 (
3584 "vertex tolerance",
3585 self.ir
3586 .model
3587 .vertices
3588 .iter()
3589 .filter(|vertex| vertex.tolerance.is_some())
3590 .count(),
3591 ),
3592 ];
3593 let topology_metadata_count = topology_metadata
3594 .iter()
3595 .map(|(_, count)| count)
3596 .sum::<usize>();
3597 if topology_metadata_count > 0 {
3598 let details = topology_metadata
3599 .iter()
3600 .filter(|(_, count)| *count > 0)
3601 .map(|(kind, count)| format!("{kind}={count}"))
3602 .collect::<Vec<_>>()
3603 .join(", ");
3604 self.loss(
3605 StepLossCode::TopologyMetadataNotRepresented,
3606 format!(
3607 "{topology_metadata_count} topology metadata value(s) were not represented in STEP: {details}"
3608 ),
3609 );
3610 }
3611 if !self.ir.model.subds.is_empty() {
3612 self.loss(
3613 StepLossCode::SubdOmitted,
3614 format!(
3615 "{} subdivision surface(s) were omitted because this STEP writer \
3616 does not encode SubD control cages",
3617 self.ir.model.subds.len()
3618 ),
3619 );
3620 }
3621 let design_arenas = [
3622 ("feature", self.ir.model.features.len()),
3623 (
3624 "feature input topology",
3625 self.ir.model.feature_input_topologies.len(),
3626 ),
3627 (
3628 "feature result topology",
3629 self.ir.model.feature_result_topologies.len(),
3630 ),
3631 ("configuration", self.ir.model.configurations.len()),
3632 ("parameter", self.ir.model.parameters.len()),
3633 ("sketch", self.ir.model.sketches.len()),
3634 ("sketch entity", self.ir.model.sketch_entities.len()),
3635 ("sketch constraint", self.ir.model.sketch_constraints.len()),
3636 ("spatial sketch", self.ir.model.spatial_sketches.len()),
3637 (
3638 "spatial sketch entity",
3639 self.ir.model.spatial_sketch_entities.len(),
3640 ),
3641 (
3642 "spatial sketch constraint",
3643 self.ir.model.spatial_sketch_constraints.len(),
3644 ),
3645 ("spreadsheet", self.ir.model.spreadsheets.len()),
3646 ];
3647 let design_record_count = design_arenas.iter().map(|(_, count)| count).sum::<usize>();
3648 if design_record_count > 0 {
3649 let details = design_arenas
3650 .iter()
3651 .filter(|(_, count)| *count > 0)
3652 .map(|(kind, count)| format!("{kind}={count}"))
3653 .collect::<Vec<_>>()
3654 .join(", ");
3655 self.loss(
3656 StepLossCode::ParametricDesignRecordsOmitted,
3657 format!(
3658 "{design_record_count} parametric/design record(s) were not represented in STEP: {details}"
3659 ),
3660 );
3661 }
3662 let presentation_arenas = [
3663 ("drawing", self.ir.model.drawings.len()),
3664 (
3665 "presentation document",
3666 self.ir.model.presentation_documents.len(),
3667 ),
3668 ("view presentation", self.ir.model.view_presentations.len()),
3669 ];
3670 let presentation_record_count = presentation_arenas
3671 .iter()
3672 .map(|(_, count)| count)
3673 .sum::<usize>();
3674 if presentation_record_count > 0 {
3675 let details = presentation_arenas
3676 .iter()
3677 .filter(|(_, count)| *count > 0)
3678 .map(|(kind, count)| format!("{kind}={count}"))
3679 .collect::<Vec<_>>()
3680 .join(", ");
3681 self.loss(
3682 StepLossCode::DrawingPresentationRecordsOmitted,
3683 format!(
3684 "{presentation_record_count} drawing/presentation record(s) were not represented in STEP: {details}"
3685 ),
3686 );
3687 }
3688 if !self.ir.model.semantic_annotations.is_empty() {
3689 self.loss(
3690 StepLossCode::SemanticAnnotationOmitted,
3691 format!(
3692 "{} semantic annotation(s) were not represented in STEP",
3693 self.ir.model.semantic_annotations.len()
3694 ),
3695 );
3696 }
3697 if !self.ir.model.assets.is_empty() {
3698 self.loss(
3699 StepLossCode::DocumentAssetOmitted,
3700 format!(
3701 "{} document asset(s) were not represented in STEP",
3702 self.ir.model.assets.len()
3703 ),
3704 );
3705 }
3706 if !self.ir.model.assembly_joints.is_empty() {
3707 self.loss(
3708 StepLossCode::AssemblyJointOmitted,
3709 format!(
3710 "{} assembly joint(s) were not represented in STEP",
3711 self.ir.model.assembly_joints.len()
3712 ),
3713 );
3714 }
3715 let non_part_products = self
3716 .ir
3717 .model
3718 .product_definitions
3719 .iter()
3720 .filter(|product| {
3721 !matches!(
3722 &product.kind,
3723 cadmpeg_ir::products::ProductDefinitionKind::Part
3724 )
3725 })
3726 .count();
3727 if non_part_products > 0 {
3728 self.loss(
3729 StepLossCode::ProductNonPartKind,
3730 format!(
3731 "{non_part_products} product definition(s) use a non-part kind not represented in STEP"
3732 ),
3733 );
3734 }
3735 let bom_property_count = self
3736 .ir
3737 .model
3738 .product_definitions
3739 .iter()
3740 .map(|product| product.bom_properties.len())
3741 .sum::<usize>();
3742 if bom_property_count > 0 {
3743 self.loss(
3744 StepLossCode::ProductBomPropertyOmitted,
3745 format!(
3746 "{bom_property_count} product BOM property value(s) were not represented in STEP"
3747 ),
3748 );
3749 }
3750 let external_occurrences = self
3751 .ir
3752 .model
3753 .occurrences
3754 .iter()
3755 .filter(|occurrence| {
3756 matches!(
3757 &occurrence.prototype,
3758 cadmpeg_ir::products::PrototypeReference::External { .. }
3759 )
3760 })
3761 .count();
3762 if external_occurrences > 0 {
3763 self.loss(
3764 StepLossCode::OccurrenceExternalProduct,
3765 format!(
3766 "{external_occurrences} occurrence(s) reference external product definitions and were not represented in STEP"
3767 ),
3768 );
3769 }
3770 let unresolved_occurrences = self
3771 .ir
3772 .model
3773 .occurrences
3774 .iter()
3775 .filter(|occurrence| {
3776 matches!(
3777 &occurrence.prototype,
3778 cadmpeg_ir::products::PrototypeReference::Unresolved
3779 )
3780 })
3781 .count();
3782 if unresolved_occurrences > 0 {
3783 self.loss(
3784 StepLossCode::OccurrenceNoWritableProduct,
3785 format!(
3786 "{unresolved_occurrences} occurrence(s) have no writable product definition"
3787 ),
3788 );
3789 }
3790 let occurrence_metadata = self
3791 .ir
3792 .model
3793 .occurrences
3794 .iter()
3795 .map(|occurrence| {
3796 usize::from(!occurrence.linked_subelements.is_empty())
3797 + usize::from(occurrence.visible.is_some())
3798 + usize::from(occurrence.element_component.is_some())
3799 + usize::from(occurrence.claim_child.is_some())
3800 + usize::from(occurrence.copy_on_change.is_some())
3801 + usize::from(occurrence.copy_on_change_source.is_some())
3802 + usize::from(occurrence.copy_on_change_group.is_some())
3803 + usize::from(occurrence.copy_on_change_touched.is_some())
3804 })
3805 .sum::<usize>();
3806 if occurrence_metadata > 0 {
3807 self.loss(
3808 StepLossCode::OccurrenceMetadataOmitted,
3809 format!(
3810 "{occurrence_metadata} occurrence metadata value(s) were not represented in STEP"
3811 ),
3812 );
3813 }
3814 let unwritten_pmi = self.ir.model.pmi.len().saturating_sub(self.written_pmi);
3815 if unwritten_pmi > 0 {
3816 self.loss(
3817 StepLossCode::PmiAnnotationNotWritten,
3818 format!("{unwritten_pmi} PMI annotation(s) were not written to STEP"),
3819 );
3820 }
3821 let source_object_count = self
3826 .ir
3827 .model
3828 .surfaces
3829 .iter()
3830 .filter(|surface| {
3831 surface
3832 .source_object
3833 .as_ref()
3834 .is_some_and(|source| source.format != "step")
3835 })
3836 .count()
3837 + self
3838 .ir
3839 .model
3840 .curves
3841 .iter()
3842 .filter(|curve| {
3843 curve
3844 .source_object
3845 .as_ref()
3846 .is_some_and(|source| source.format != "step")
3847 })
3848 .count()
3849 + self
3850 .ir
3851 .model
3852 .subds
3853 .iter()
3854 .filter(|subd| {
3855 subd.source_object
3856 .as_ref()
3857 .is_some_and(|source| source.format != "step")
3858 })
3859 .count()
3860 + self
3861 .ir
3862 .model
3863 .tessellations
3864 .iter()
3865 .filter(|tessellation| {
3866 tessellation
3867 .source_object
3868 .as_ref()
3869 .is_some_and(|source| source.format != "step")
3870 })
3871 .count();
3872 let source_object_count = source_object_count
3873 + self
3874 .ir
3875 .model
3876 .points
3877 .iter()
3878 .filter(|point| {
3879 point
3880 .source_object
3881 .as_ref()
3882 .is_some_and(|source| source.format != "step")
3883 })
3884 .count();
3885 if source_object_count > 0 {
3886 self.loss(
3887 StepLossCode::SourceAssociationOmitted,
3888 format!(
3889 "{source_object_count} source-object association(s) were not represented in STEP"
3890 ),
3891 );
3892 }
3893 let unknown_count = self
3894 .ir
3895 .native
3896 .loss_counts()
3897 .into_iter()
3898 .filter(|count| count.kind == "unknowns")
3899 .map(|count| count.count)
3900 .sum::<usize>();
3901 if unknown_count > 0 {
3902 self.loss(
3903 StepLossCode::PassthroughRecordOmitted,
3904 format!("{unknown_count} uninterpreted passthrough record(s) were not represented in STEP"),
3905 );
3906 }
3907 if self.unstyled_colors > 0 {
3908 self.loss(
3909 StepLossCode::DisplayColorUnstyled,
3910 format!(
3911 "{} display color(s) had no emitted STEP item and were not written \
3912 to STEP presentation",
3913 self.unstyled_colors
3914 ),
3915 );
3916 }
3917 let lossy_appearances = self
3918 .ir
3919 .model
3920 .appearances
3921 .iter()
3922 .filter(|appearance| {
3923 let bindings = self
3924 .ir
3925 .model
3926 .appearance_bindings
3927 .iter()
3928 .filter(|binding| binding.appearance == appearance.id)
3929 .collect::<Vec<_>>();
3930 appearance.asset_guid.is_some()
3931 || appearance.visual_guid.is_some()
3932 || appearance.physical_token.is_some()
3933 || appearance
3934 .schema
3935 .as_deref()
3936 .is_some_and(|schema| schema != "step_surface_style")
3937 || appearance.category.is_some()
3938 || !appearance.properties.is_empty()
3939 || appearance.base_color.is_none_or(|color| color.a != 1.0)
3940 || bindings.is_empty()
3941 || bindings
3942 .iter()
3943 .any(|binding| !self.written_appearance_bindings.contains(&binding.id))
3944 })
3945 .count();
3946 if lossy_appearances > 0 {
3947 self.loss(
3948 StepLossCode::AppearanceReducedToBaseColor,
3949 format!(
3950 "{lossy_appearances} appearance asset(s) were reduced to STYLED_ITEM base colors; \
3951 schemas, textures, and shader properties were not written to STEP"
3952 ),
3953 );
3954 }
3955 let lossy_binding_metadata = self
3956 .ir
3957 .model
3958 .appearance_bindings
3959 .iter()
3960 .filter(|binding| binding.object_type.is_some() || !binding.channels.is_empty())
3961 .count();
3962 if lossy_binding_metadata > 0 {
3963 self.loss(
3964 StepLossCode::AppearanceBindingMetadataReduced,
3965 format!(
3966 "{lossy_binding_metadata} appearance binding(s) carry source object or channel metadata not represented in STEP"
3967 ),
3968 );
3969 }
3970 if !self.ir.model.attributes.is_empty() {
3971 self.loss(
3972 StepLossCode::SourceAttributeNotWritten,
3973 format!(
3974 "{} source attribute record(s) were not written to STEP",
3975 self.ir.model.attributes.len()
3976 ),
3977 );
3978 }
3979 let procedural_surface_count = self
3980 .ir
3981 .model
3982 .procedural_surfaces
3983 .iter()
3984 .filter(|procedural| !self.written_procedural_surfaces.contains(&procedural.id.0))
3985 .count();
3986 let procedural_curve_count = self
3987 .ir
3988 .model
3989 .procedural_curves
3990 .iter()
3991 .filter(|procedural| !self.written_procedural_curves.contains(&procedural.id.0))
3992 .count();
3993 if procedural_surface_count > 0 || procedural_curve_count > 0 {
3994 self.loss(
3995 StepLossCode::ProceduralReducedToCarrier,
3996 format!(
3997 "{procedural_surface_count} procedural surface definition(s) and {procedural_curve_count} procedural curve definition(s) were reduced to their solved STEP carriers"
3998 ),
3999 );
4000 }
4001 let source_native_records: usize = self
4002 .ir
4003 .native
4004 .loss_counts()
4005 .iter()
4006 .filter(|loss| loss.kind != "unknowns")
4007 .map(|loss| loss.count)
4008 .sum();
4009 if source_native_records > 0 {
4010 self.loss(
4011 StepLossCode::SourceNativeRecordOmitted,
4012 format!(
4013 "{source_native_records} source-native record(s) were not represented in STEP"
4014 ),
4015 );
4016 }
4017 }
4018
4019 fn finish_report(&self) -> ExportReport {
4020 ExportReport {
4021 format: "step".into(),
4022 census: cadmpeg_ir::EntityCensus {
4023 basis: cadmpeg_ir::CensusBasis::TargetRecords,
4024 counts: self.emitter.counts(),
4025 },
4026 fidelity: FidelityResolution::NotProvided,
4027 write_path: WritePath::Synthesized,
4030 losses: self.losses.clone(),
4031 notes: self.notes.clone(),
4032 }
4033 }
4034}
4035
4036fn is_identity(rows: &[[f64; 4]; 4]) -> bool {
4037 for (i, row) in rows.iter().enumerate() {
4038 for (j, &v) in row.iter().enumerate() {
4039 let expect = if i == j { 1.0 } else { 0.0 };
4040 if (v - expect).abs() > EPS_IDENTITY {
4041 return false;
4042 }
4043 }
4044 }
4045 true
4046}
4047
4048pub(crate) fn is_rigid_transform(rows: &[[f64; 4]; 4]) -> bool {
4049 cadmpeg_ir::transform::Transform { rows: *rows }.is_proper_rigid()
4050}