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