1#![cfg_attr(test, allow(clippy::unwrap_used))]
3mod geometry;
52pub mod lex;
53pub mod parse;
54mod reader;
55pub mod strings;
56mod writer;
57
58use std::collections::{BTreeMap, BTreeSet, HashMap};
59use std::io::Write;
60
61use cadmpeg_core::{CodecError, ContainerEntry, ContainerSummary};
62use cadmpeg_ir::appearance::Appearance;
63use cadmpeg_ir::codec::{
64 Codec, Confidence, DecodeOptions, DecodeResult, EncodeInput, Encoder, ExportPlan,
65};
66use cadmpeg_ir::geometry::{
67 Curve, CurveGeometry, Pcurve, ProceduralCurve, ProceduralCurveDefinition, ProceduralSurface,
68 ProceduralSurfaceDefinition, Surface, SurfaceGeometry,
69};
70use cadmpeg_ir::ids::{OccurrenceId, ProductDefinitionId};
71use cadmpeg_ir::products::{AssemblyGraph, OccurrenceParent, PrototypeReference};
72use cadmpeg_ir::report::{ExportReport, LossKind, LossNote, Severity};
73use cadmpeg_ir::topology::{
74 Body, BodyKind, Coedge, Edge, Face, Loop, LoopBoundaryRole, Point, Sense, Shell, Vertex,
75};
76use cadmpeg_ir::CadIr;
77use cadmpeg_ir::{FidelityResolution, WritePath};
78
79use writer::{real, refs, string, Emitter, Ref};
80
81#[derive(Debug, Clone)]
88pub struct StepWriteOptions {
89 pub schema: StepSchema,
91 pub unsupported: StepUnsupportedPolicy,
93 pub product_name: String,
98 pub author: String,
100 pub organization: String,
102 pub timestamp: String,
107 pub originating_system: String,
109}
110
111impl Default for StepWriteOptions {
112 fn default() -> Self {
113 StepWriteOptions {
114 schema: StepSchema::Ap214,
115 unsupported: StepUnsupportedPolicy::Report,
116 product_name: "cadmpeg_model".to_string(),
117 author: String::new(),
118 organization: String::new(),
119 timestamp: String::new(),
120 originating_system: "cadmpeg".to_string(),
121 }
122 }
123}
124
125#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
127pub enum StepUnsupportedPolicy {
128 #[default]
130 Report,
131 Reject,
133}
134
135#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
140pub enum StepSchema {
141 Ap203Edition1,
143 Ap203Edition2,
145 #[default]
147 Ap214,
148 Ap242Edition1,
150 Ap242Edition2,
152 Ap242Edition3,
154}
155
156impl StepSchema {
157 pub const fn file_schema(self) -> &'static str {
159 match self {
160 Self::Ap203Edition1 => "CONFIG_CONTROL_DESIGN",
161 Self::Ap203Edition2 => "AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF { 1 0 10303 403 2 1 2 }",
162 Self::Ap214 => "AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }",
163 Self::Ap242Edition1 => "AP242_MANAGED_MODEL_BASED_3D_ENGINEERING_MIM_LF { 1 0 10303 442 1 1 4 }",
164 Self::Ap242Edition2 => "AP242_MANAGED_MODEL_BASED_3D_ENGINEERING_MIM_LF { 1 0 10303 442 3 1 4 }",
165 Self::Ap242Edition3 => "AP242_MANAGED_MODEL_BASED_3D_ENGINEERING_MIM_LF { 1 0 10303 442 4 1 4 }",
166 }
167 }
168
169 const fn supports_tessellation(self) -> bool {
170 matches!(
171 self,
172 Self::Ap242Edition1 | Self::Ap242Edition2 | Self::Ap242Edition3
173 )
174 }
175
176 const fn supports_semantic_pmi(self) -> bool {
177 self.supports_tessellation()
178 }
179
180 const fn supports_visibility(self) -> bool {
181 !matches!(self, Self::Ap203Edition1)
182 }
183
184 const fn application_protocol(self) -> (&'static str, &'static str, i32) {
185 match self {
186 Self::Ap203Edition1 => (
187 "configuration controlled 3d designs of mechanical parts and assemblies",
188 "config_control_design",
189 1994,
190 ),
191 Self::Ap203Edition2 => (
192 "configuration controlled 3d designs of mechanical parts and assemblies",
193 "ap203_configuration_controlled_3d_design_of_mechanical_parts_and_assemblies",
194 2011,
195 ),
196 Self::Ap214 => ("automotive design", "automotive_design", 2000),
197 Self::Ap242Edition1 => (
198 "managed model based 3d engineering",
199 "ap242_managed_model_based_3d_engineering",
200 2014,
201 ),
202 Self::Ap242Edition2 => (
203 "managed model based 3d engineering",
204 "ap242_managed_model_based_3d_engineering",
205 2020,
206 ),
207 Self::Ap242Edition3 => (
208 "managed model based 3d engineering",
209 "ap242_managed_model_based_3d_engineering",
210 2022,
211 ),
212 }
213 }
214}
215
216#[derive(Debug, thiserror::Error)]
221pub enum StepError {
222 #[error("STEP target cannot represent the document without loss: {0}")]
224 Unsupported(String),
225 #[error("failed to write STEP output: {0}")]
227 Io(#[from] std::io::Error),
228}
229
230pub fn write_step(
246 ir: &CadIr,
247 w: &mut (impl Write + ?Sized),
248 opts: &StepWriteOptions,
249) -> Result<ExportReport, StepError> {
250 let mut b = Builder::new(ir, opts.schema);
251 b.build();
252 let report = b.finish_report();
253 let lines = b.emitter.into_lines();
254
255 if opts.unsupported == StepUnsupportedPolicy::Reject && !report.losses.is_empty() {
256 return Err(StepError::Unsupported(
257 report
258 .losses
259 .iter()
260 .map(|loss| loss.message.as_str())
261 .collect::<Vec<_>>()
262 .join("; "),
263 ));
264 }
265
266 write_header(w, opts)?;
267 writeln!(w, "DATA;")?;
268 for line in &lines {
269 writeln!(w, "{line}")?;
270 }
271 writeln!(w, "ENDSEC;")?;
272 writeln!(w, "END-ISO-10303-21;")?;
273 Ok(report)
274}
275
276fn write_header(w: &mut (impl Write + ?Sized), opts: &StepWriteOptions) -> std::io::Result<()> {
277 let ts = if opts.timestamp.is_empty() {
278 "1970-01-01T00:00:00"
279 } else {
280 &opts.timestamp
281 };
282 writeln!(w, "ISO-10303-21;")?;
283 writeln!(w, "HEADER;")?;
284 writeln!(
285 w,
286 "FILE_DESCRIPTION(({}),'2;1');",
287 string("CAD model exported by cadmpeg")
288 )?;
289 writeln!(
290 w,
291 "FILE_NAME({},{},({}),({}),{},{},{});",
292 string(&opts.product_name),
293 string(ts),
294 string(&opts.author),
295 string(&opts.organization),
296 string("cadmpeg-step"),
297 string(&opts.originating_system),
298 string("")
299 )?;
300 writeln!(w, "FILE_SCHEMA(({}));", string(opts.schema.file_schema()))?;
301 writeln!(w, "ENDSEC;")?;
302 Ok(())
303}
304
305#[derive(Clone, Copy)]
306struct ColorSpec<'a> {
307 color: cadmpeg_ir::topology::Color,
308 appearance: Option<&'a Appearance>,
309 binding_id: Option<&'a str>,
310}
311
312struct Builder<'a> {
313 ir: &'a CadIr,
314 schema: StepSchema,
315 emitter: Emitter,
316 losses: Vec<LossNote>,
317 notes: Vec<String>,
318
319 points: HashMap<&'a str, &'a Point>,
320 bodies: HashMap<&'a str, &'a Body>,
321 shells: HashMap<&'a str, &'a Shell>,
322 faces: HashMap<&'a str, &'a Face>,
323 loops: HashMap<&'a str, &'a Loop>,
324 vertices: HashMap<&'a str, &'a Vertex>,
325 edges: HashMap<&'a str, &'a Edge>,
326 coedges: HashMap<&'a str, &'a Coedge>,
327 surfaces: HashMap<&'a str, &'a Surface>,
328 curves: HashMap<&'a str, &'a Curve>,
329 pcurves: HashMap<&'a str, &'a Pcurve>,
330 procedural_surfaces: HashMap<&'a str, &'a ProceduralSurface>,
331 procedural_curves: HashMap<&'a str, &'a ProceduralCurve>,
332 edge_coedges: HashMap<&'a str, Vec<(&'a str, &'a str)>>,
333
334 surface_refs: HashMap<String, Ref>,
335 curve_refs: HashMap<String, Ref>,
336 edge_refs: HashMap<String, Ref>,
337 vertex_refs: HashMap<String, Ref>,
338 point_refs: HashMap<String, Ref>,
339 pcurve_context: Option<Ref>,
340 active_surfaces: BTreeSet<String>,
341 active_curves: BTreeSet<String>,
342 written_procedural_surfaces: BTreeSet<String>,
343 written_procedural_curves: BTreeSet<String>,
344
345 curveless_edges: BTreeSet<String>,
349
350 unknown_surface_faces: BTreeSet<String>,
354
355 topology_relation_losses: BTreeSet<String>,
357
358 face_step_refs: HashMap<String, Ref>,
359 body_step_refs: HashMap<String, Ref>,
361 default_product_definition_shape: Option<Ref>,
362 body_shape_refs: HashMap<String, Ref>,
363 body_item_refs: HashMap<String, Vec<Ref>>,
364 tessellation_step_refs: HashMap<String, Ref>,
365 written_appearance_bindings: BTreeSet<String>,
366 unstyled_colors: usize,
367 unsupported_standalone_geometry: usize,
368 written_pmi: usize,
369 length_unit: Option<Ref>,
370 angle_unit: Option<Ref>,
371 ratio_unit: Option<Ref>,
372 geometry_emission_depth: usize,
373}
374
375impl<'a> Builder<'a> {
376 fn new(ir: &'a CadIr, schema: StepSchema) -> Self {
377 let loop_surfaces = ir
378 .model
379 .faces
380 .iter()
381 .flat_map(|face| {
382 face.loops
383 .iter()
384 .map(move |loop_id| (loop_id.as_str(), face.surface.as_str()))
385 })
386 .collect::<HashMap<_, _>>();
387 let coedge_surfaces: HashMap<&str, &str> = ir
388 .model
389 .loops
390 .iter()
391 .filter_map(|loop_| {
392 loop_surfaces
393 .get(loop_.id.as_str())
394 .map(|surface| (loop_, *surface))
395 })
396 .flat_map(|(loop_, surface)| {
397 loop_
398 .coedges
399 .iter()
400 .map(move |coedge| (coedge.as_str(), surface))
401 })
402 .collect();
403 let mut edge_coedges = HashMap::<&str, Vec<(&str, &str)>>::new();
404 for coedge in &ir.model.coedges {
405 let Some(surface) = coedge_surfaces.get(coedge.id.as_str()) else {
406 continue;
407 };
408 for pcurve in &coedge.pcurves {
409 edge_coedges
410 .entry(coedge.edge.as_str())
411 .or_default()
412 .push((pcurve.pcurve.as_str(), *surface));
413 }
414 }
415 Builder {
416 ir,
417 schema,
418 emitter: Emitter::new(),
419 losses: Vec::new(),
420 notes: Vec::new(),
421 points: ir.model.points.iter().map(|p| (p.id.as_str(), p)).collect(),
422 bodies: ir
423 .model
424 .bodies
425 .iter()
426 .map(|body| (body.id.as_str(), body))
427 .collect(),
428 shells: ir
429 .model
430 .shells
431 .iter()
432 .map(|shell| (shell.id.as_str(), shell))
433 .collect(),
434 faces: ir
435 .model
436 .faces
437 .iter()
438 .map(|face| (face.id.as_str(), face))
439 .collect(),
440 loops: ir
441 .model
442 .loops
443 .iter()
444 .map(|loop_| (loop_.id.as_str(), loop_))
445 .collect(),
446 vertices: ir
447 .model
448 .vertices
449 .iter()
450 .map(|v| (v.id.as_str(), v))
451 .collect(),
452 edges: ir.model.edges.iter().map(|e| (e.id.as_str(), e)).collect(),
453 coedges: ir
454 .model
455 .coedges
456 .iter()
457 .map(|c| (c.id.as_str(), c))
458 .collect(),
459 surfaces: ir
460 .model
461 .surfaces
462 .iter()
463 .map(|s| (s.id.as_str(), s))
464 .collect(),
465 curves: ir.model.curves.iter().map(|c| (c.id.as_str(), c)).collect(),
466 pcurves: ir
467 .model
468 .pcurves
469 .iter()
470 .map(|p| (p.id.as_str(), p))
471 .collect(),
472 procedural_surfaces: ir
473 .model
474 .procedural_surfaces
475 .iter()
476 .map(|surface| (surface.surface.as_str(), surface))
477 .collect(),
478 procedural_curves: ir
479 .model
480 .procedural_curves
481 .iter()
482 .map(|curve| (curve.curve.as_str(), curve))
483 .collect(),
484 edge_coedges,
485 surface_refs: HashMap::new(),
486 curve_refs: HashMap::new(),
487 edge_refs: HashMap::new(),
488 vertex_refs: HashMap::new(),
489 point_refs: HashMap::new(),
490 pcurve_context: None,
491 active_surfaces: BTreeSet::new(),
492 active_curves: BTreeSet::new(),
493 written_procedural_surfaces: BTreeSet::new(),
494 written_procedural_curves: BTreeSet::new(),
495 curveless_edges: BTreeSet::new(),
496 unknown_surface_faces: BTreeSet::new(),
497 topology_relation_losses: BTreeSet::new(),
498 face_step_refs: HashMap::new(),
499 body_step_refs: HashMap::new(),
500 default_product_definition_shape: None,
501 body_shape_refs: HashMap::new(),
502 body_item_refs: HashMap::new(),
503 tessellation_step_refs: HashMap::new(),
504 written_appearance_bindings: BTreeSet::new(),
505 unstyled_colors: 0,
506 unsupported_standalone_geometry: 0,
507 written_pmi: 0,
508 length_unit: None,
509 angle_unit: None,
510 ratio_unit: None,
511 geometry_emission_depth: 0,
512 }
513 }
514
515 fn loss(&mut self, code: LossKind, severity: Severity, message: String) {
516 self.losses.push(LossNote {
517 code,
518 severity,
519 message,
520 provenance: None,
521 });
522 }
523
524 fn omit(&mut self, code: LossKind, severity: Severity, message: String) {
525 self.losses.push(LossNote {
526 code,
527 severity,
528 message,
529 provenance: None,
530 });
531 }
532
533 fn topology_relation_loss(&mut self, identity: String, severity: Severity, message: String) {
534 if self.topology_relation_losses.insert(identity) {
535 self.loss(LossKind::TopologyNotTransferred, severity, message);
536 }
537 }
538
539 fn build(&mut self) {
540 let context = self.emit_context();
541
542 let shape_items = self.emit_shape_items(context);
543 let mut standalone_items = self.emit_standalone_geometry();
544 let has_standalone_geometry = !standalone_items.is_empty();
545 let mut emitted_items = shape_items;
546 emitted_items.extend(standalone_items.iter().copied());
547 if emitted_items.is_empty() && !self.ir.model.bodies.is_empty() {
548 self.losses.push(LossNote {
549 code: LossKind::NoExportableSolids,
550 severity: Severity::Warning,
551 message: "no exportable solids: the IR document contains no body/region/shell \
552 geometry, so the STEP representation is empty"
553 .to_string(),
554 provenance: None,
555 });
556 emitted_items.clear();
557 }
558 let mut items = emitted_items;
559 let origin = geometry::placement(
560 &mut self.emitter,
561 cadmpeg_ir::math::Point3::new(0.0, 0.0, 0.0),
562 cadmpeg_ir::math::Vector3::new(0.0, 0.0, 1.0),
563 cadmpeg_ir::math::Vector3::new(1.0, 0.0, 0.0),
564 );
565 items.push(origin);
566
567 if self.ir.model.product_definitions.is_empty() {
568 let product_def_shape = self.emit_product_structure();
569 self.default_product_definition_shape = Some(product_def_shape);
570 let representation_kind = if !has_standalone_geometry
571 && !self.ir.model.bodies.is_empty()
572 && self.ir.model.bodies.iter().all(|body| {
573 body.transform
574 .is_none_or(|transform| is_identity(&transform.rows))
575 })
576 && self
577 .ir
578 .model
579 .bodies
580 .iter()
581 .all(|body| body.kind == BodyKind::Solid)
582 {
583 "ADVANCED_BREP_SHAPE_REPRESENTATION"
584 } else {
585 "SHAPE_REPRESENTATION"
586 };
587 let representation = self.emitter.emit(
588 representation_kind,
589 &format!("'',{},{context}", refs(&items)),
590 );
591 self.emitter.emit(
592 "SHAPE_DEFINITION_REPRESENTATION",
593 &format!("{product_def_shape},{representation}"),
594 );
595 } else {
596 self.emit_product_graph(context);
597 if has_standalone_geometry {
598 standalone_items.push(origin);
599 self.emitter.emit(
600 "SHAPE_REPRESENTATION",
601 &format!(
602 "{},{},{context}",
603 string("standalone geometry"),
604 refs(&standalone_items)
605 ),
606 );
607 }
608 }
609
610 self.emit_visibility();
611 self.emit_tessellations(context);
612 self.emit_presentation(context);
613 self.emit_layers();
614 self.emit_pmi(context);
615 self.note_unrepresented();
616 }
617
618 fn emit_presentation(&mut self, context: Ref) {
619 use cadmpeg_ir::appearance::AppearanceTarget;
620
621 let ir = self.ir;
622 let appearances: HashMap<&str, &Appearance> = ir
623 .model
624 .appearances
625 .iter()
626 .map(|appearance| (appearance.id.as_str(), appearance))
627 .collect();
628 let mut body_colors: HashMap<&str, ColorSpec<'_>> = HashMap::new();
629 let mut face_colors: HashMap<&str, ColorSpec<'_>> = HashMap::new();
630 for binding in &ir.model.appearance_bindings {
631 let Some(appearance) = appearances.get(binding.appearance.as_str()).copied() else {
632 continue;
633 };
634 let Some(color) = appearance.base_color else {
635 continue;
636 };
637 let spec = ColorSpec {
638 color,
639 appearance: Some(appearance),
640 binding_id: Some(&binding.id),
641 };
642 match &binding.target {
643 AppearanceTarget::Body(id) => {
644 body_colors.entry(id.as_str()).or_insert(spec);
645 }
646 AppearanceTarget::Face(id) => {
647 face_colors.entry(id.as_str()).or_insert(spec);
648 }
649 AppearanceTarget::Surface(_)
650 | AppearanceTarget::Curve(_)
651 | AppearanceTarget::Point(_)
652 | AppearanceTarget::Edge(_)
653 | AppearanceTarget::Vertex(_)
654 | AppearanceTarget::Tessellation(_)
655 | AppearanceTarget::Source { .. } => {}
656 }
657 }
658 for body in &ir.model.bodies {
659 if let Some(color) = body.color {
660 body_colors.entry(body.id.as_str()).or_insert(ColorSpec {
661 color,
662 appearance: None,
663 binding_id: None,
664 });
665 }
666 }
667 for face in &ir.model.faces {
668 if let Some(color) = face.color {
669 face_colors.entry(face.id.as_str()).or_insert(ColorSpec {
670 color,
671 appearance: None,
672 binding_id: None,
673 });
674 }
675 }
676
677 let mut face_body: HashMap<&str, &str> = HashMap::new();
678 for region in &ir.model.regions {
679 let body = region.body.0.as_str();
680 for shell_id in ®ion.shells {
681 let Some(shell) = self.shells.get(shell_id.as_str()).copied() else {
682 continue;
683 };
684 for face in &shell.faces {
685 face_body.insert(face.0.as_str(), body);
686 }
687 }
688 }
689
690 let mut style_refs: HashMap<String, Ref> = HashMap::new();
697 let mut styled = Vec::new();
698 let mut faces: Vec<(String, Ref)> = self
699 .face_step_refs
700 .iter()
701 .map(|(id, r)| (id.clone(), *r))
702 .collect();
703 faces.sort_by(|a, b| a.0.cmp(&b.0));
704 let mut styled_bodies: BTreeSet<&str> = BTreeSet::new();
705 for (face_id, face) in &faces {
706 let own = face_colors.get(face_id.as_str()).copied();
707 let body = face_body.get(face_id.as_str()).copied();
708 let inherited = body.and_then(|b| body_colors.get(b).copied());
709 let Some(spec) = own.or(inherited) else {
710 continue;
711 };
712 if own.is_none() {
715 if let Some(b) = body {
716 styled_bodies.insert(b);
717 }
718 }
719 if let Some(binding_id) = spec.binding_id {
720 self.written_appearance_bindings
721 .insert(binding_id.to_string());
722 }
723 let name = spec
724 .appearance
725 .and_then(|appearance| appearance.name.as_deref())
726 .unwrap_or("");
727 let style = self.surface_style(spec.color, name, &mut style_refs);
728 styled.push(
729 self.emitter
730 .emit("STYLED_ITEM", &format!("'color',({style}),{face}")),
731 );
732 }
733 let mut direct_unstyled = BTreeSet::new();
734 for binding in &ir.model.appearance_bindings {
735 if self.written_appearance_bindings.contains(&binding.id) {
736 continue;
737 }
738 let Some(appearance) = appearances.get(binding.appearance.as_str()).copied() else {
739 continue;
740 };
741 let Some(color) = appearance.base_color else {
742 continue;
743 };
744 let (target, style_kind) = match &binding.target {
745 AppearanceTarget::Face(id) => {
746 (self.face_step_refs.get(id.as_str()).copied(), "surface")
747 }
748 AppearanceTarget::Surface(id) => {
749 (self.surface_refs.get(id.as_str()).copied(), "surface")
750 }
751 AppearanceTarget::Curve(id) => (self.curve_refs.get(id.as_str()).copied(), "curve"),
752 AppearanceTarget::Edge(id) => (self.edge_refs.get(id.as_str()).copied(), "curve"),
753 AppearanceTarget::Point(id) => (self.point_refs.get(id.as_str()).copied(), "point"),
754 AppearanceTarget::Tessellation(id) => {
755 (self.tessellation_step_refs.get(id).copied(), "surface")
756 }
757 AppearanceTarget::Body(_)
758 | AppearanceTarget::Vertex(_)
759 | AppearanceTarget::Source { .. } => continue,
760 };
761 let Some(target) = target else {
762 let target_id = match &binding.target {
763 AppearanceTarget::Face(id) => id.0.clone(),
764 AppearanceTarget::Surface(id) => id.0.clone(),
765 AppearanceTarget::Curve(id) => id.0.clone(),
766 AppearanceTarget::Edge(id) => id.0.clone(),
767 AppearanceTarget::Point(id) => id.0.clone(),
768 AppearanceTarget::Tessellation(id) => id.clone(),
769 AppearanceTarget::Body(_)
770 | AppearanceTarget::Vertex(_)
771 | AppearanceTarget::Source { .. } => continue,
772 };
773 direct_unstyled.insert(target_id);
774 continue;
775 };
776 let name = appearance.name.as_deref().unwrap_or("");
777 let style = match style_kind {
778 "surface" => self.surface_style(color, name, &mut style_refs),
779 "curve" => self.curve_style(color, name, &mut style_refs),
780 "point" => self.point_style(color, name, &mut style_refs),
781 _ => unreachable!(),
782 };
783 self.written_appearance_bindings.insert(binding.id.clone());
784 styled.push(
785 self.emitter
786 .emit("STYLED_ITEM", &format!("'color',({style}),{target}")),
787 );
788 }
789 let emitted: BTreeSet<&str> = self.face_step_refs.keys().map(String::as_str).collect();
793 let mut unstyled_targets = face_colors
794 .keys()
795 .filter(|id| !emitted.contains(**id as &str))
796 .map(|id| (*id).to_string())
797 .collect::<BTreeSet<_>>();
798 unstyled_targets.extend(
799 body_colors
800 .keys()
801 .filter(|id| !styled_bodies.contains(**id as &str))
802 .map(|id| (*id).to_string()),
803 );
804 unstyled_targets.extend(direct_unstyled);
805 self.unstyled_colors = unstyled_targets.len();
806 if styled.is_empty() {
807 return;
808 }
809 self.emitter.emit(
810 "MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION",
811 &format!("'',{},{context}", refs(&styled)),
812 );
813 }
814
815 fn surface_style(
816 &mut self,
817 color: cadmpeg_ir::topology::Color,
818 name: &str,
819 cache: &mut HashMap<String, Ref>,
820 ) -> Ref {
821 let rgb = format!(
822 "{},{},{}",
823 real(f64::from(color.r)),
824 real(f64::from(color.g)),
825 real(f64::from(color.b))
826 );
827 let key = format!("surface:{name}:{rgb}");
828 if let Some(style) = cache.get(&key) {
829 return *style;
830 }
831 let colour = self
832 .emitter
833 .emit("COLOUR_RGB", &format!("{},{rgb}", string(name)));
834 let fill_colour = self
835 .emitter
836 .emit("FILL_AREA_STYLE_COLOUR", &format!("'',{colour}"));
837 let fill = self
838 .emitter
839 .emit("FILL_AREA_STYLE", &format!("'',({fill_colour})"));
840 let style_fill = self
841 .emitter
842 .emit("SURFACE_STYLE_FILL_AREA", &fill.to_string());
843 let side = self
844 .emitter
845 .emit("SURFACE_SIDE_STYLE", &format!("'',({style_fill})"));
846 let usage = self
847 .emitter
848 .emit("SURFACE_STYLE_USAGE", &format!(".BOTH.,{side}"));
849 let assignment = self
850 .emitter
851 .emit("PRESENTATION_STYLE_ASSIGNMENT", &format!("({usage})"));
852 cache.insert(key, assignment);
853 assignment
854 }
855
856 fn curve_style(
857 &mut self,
858 color: cadmpeg_ir::topology::Color,
859 name: &str,
860 cache: &mut HashMap<String, Ref>,
861 ) -> Ref {
862 let rgb = format!(
863 "{},{},{}",
864 real(f64::from(color.r)),
865 real(f64::from(color.g)),
866 real(f64::from(color.b))
867 );
868 let key = format!("curve:{name}:{rgb}");
869 if let Some(style) = cache.get(&key) {
870 return *style;
871 }
872 let colour = self
873 .emitter
874 .emit("COLOUR_RGB", &format!("{},{rgb}", string(name)));
875 let font = self
876 .emitter
877 .emit("DRAUGHTING_PRE_DEFINED_CURVE_FONT", &string("continuous"));
878 let curve = self.emitter.emit(
879 "CURVE_STYLE",
880 &format!("'',{font},POSITIVE_LENGTH_MEASURE(0.1),{colour}"),
881 );
882 let assignment = self
883 .emitter
884 .emit("PRESENTATION_STYLE_ASSIGNMENT", &format!("({curve})"));
885 cache.insert(key, assignment);
886 assignment
887 }
888
889 fn point_style(
890 &mut self,
891 color: cadmpeg_ir::topology::Color,
892 name: &str,
893 cache: &mut HashMap<String, Ref>,
894 ) -> Ref {
895 let rgb = format!(
896 "{},{},{}",
897 real(f64::from(color.r)),
898 real(f64::from(color.g)),
899 real(f64::from(color.b))
900 );
901 let key = format!("point:{name}:{rgb}");
902 if let Some(style) = cache.get(&key) {
903 return *style;
904 }
905 let colour = self
906 .emitter
907 .emit("COLOUR_RGB", &format!("{},{rgb}", string(name)));
908 let point = self.emitter.emit(
909 "POINT_STYLE",
910 &format!("'',.DOT.,POSITIVE_LENGTH_MEASURE(1.),{colour}"),
911 );
912 let assignment = self
913 .emitter
914 .emit("PRESENTATION_STYLE_ASSIGNMENT", &format!("({point})"));
915 cache.insert(key, assignment);
916 assignment
917 }
918
919 fn emit_layers(&mut self) {
920 use cadmpeg_ir::presentation::PresentationItem;
921
922 for layer in self.ir.model.presentation_layers.clone() {
923 let mut assigned = Vec::new();
924 let mut unsupported = 0usize;
925 for item in layer.items {
926 let reference = match item {
927 PresentationItem::Body { body } => {
928 self.body_shape_refs.get(body.as_str()).copied()
929 }
930 PresentationItem::Face { face } => {
931 self.face_step_refs.get(face.as_str()).copied()
932 }
933 PresentationItem::Edge { edge } => self.edge_refs.get(edge.as_str()).copied(),
934 PresentationItem::Vertex { vertex } => {
935 self.vertex_refs.get(vertex.as_str()).copied()
936 }
937 PresentationItem::Curve { curve } => {
938 self.curve_refs.get(curve.as_str()).copied()
939 }
940 PresentationItem::Surface { surface } => {
941 self.surface_refs.get(surface.as_str()).copied()
942 }
943 PresentationItem::Point { .. }
944 | PresentationItem::Product { .. }
945 | PresentationItem::Occurrence { .. }
946 | PresentationItem::Pmi { .. }
947 | PresentationItem::Tessellation { .. }
948 | PresentationItem::Source { .. } => None,
949 };
950 if let Some(reference) = reference {
951 assigned.push(reference);
952 } else {
953 unsupported += 1;
954 }
955 }
956 if unsupported > 0 {
957 self.loss(
958 LossKind::AttributesNotTransferred,
959 Severity::Warning,
960 format!(
961 "layer '{}' has {unsupported} item(s) without a writable STEP carrier",
962 layer.name
963 ),
964 );
965 }
966 if !assigned.is_empty() {
967 self.emitter.emit(
968 "PRESENTATION_LAYER_ASSIGNMENT",
969 &format!(
970 "{},{},{}",
971 string(&layer.name),
972 string(layer.description.as_deref().unwrap_or("")),
973 refs(&assigned)
974 ),
975 );
976 }
977 }
978 }
979
980 fn emit_product_structure(&mut self) -> Ref {
981 let name = self
982 .ir
983 .model
984 .bodies
985 .first()
986 .and_then(|b| b.name.clone())
987 .unwrap_or_else(|| "cadmpeg_model".to_string());
988
989 let (application, protocol, year) = self.schema.application_protocol();
990 let app_ctx = self
991 .emitter
992 .emit("APPLICATION_CONTEXT", &string(application));
993 self.emitter.emit(
994 "APPLICATION_PROTOCOL_DEFINITION",
995 &format!(
996 "{},{},{year},{app_ctx}",
997 string("international standard"),
998 string(protocol)
999 ),
1000 );
1001 let prod_ctx = self.emitter.emit(
1002 "PRODUCT_CONTEXT",
1003 &format!("'',{app_ctx},{}", string("mechanical")),
1004 );
1005 let product = self.emitter.emit(
1006 "PRODUCT",
1007 &format!("{},{},'',({prod_ctx})", string(&name), string(&name)),
1008 );
1009 let formation = self
1010 .emitter
1011 .emit("PRODUCT_DEFINITION_FORMATION", &format!("'','',{product}"));
1012 let pd_ctx = self.emitter.emit(
1013 "PRODUCT_DEFINITION_CONTEXT",
1014 &format!(
1015 "{},{app_ctx},{}",
1016 string("part definition"),
1017 string("design")
1018 ),
1019 );
1020 let product_def = self.emitter.emit(
1021 "PRODUCT_DEFINITION",
1022 &format!("{},'',{formation},{pd_ctx}", string("design")),
1023 );
1024 self.emitter
1025 .emit("PRODUCT_DEFINITION_SHAPE", &format!("'','',{product_def}"))
1026 }
1027
1028 fn emit_product_graph(&mut self, context: Ref) {
1029 let (application, protocol, year) = self.schema.application_protocol();
1030 let app_context = self
1031 .emitter
1032 .emit("APPLICATION_CONTEXT", &string(application));
1033 self.emitter.emit(
1034 "APPLICATION_PROTOCOL_DEFINITION",
1035 &format!(
1036 "{},{},{year},{app_context}",
1037 string("international standard"),
1038 string(protocol)
1039 ),
1040 );
1041 let product_context = self.emitter.emit(
1042 "PRODUCT_CONTEXT",
1043 &format!("'',{app_context},{}", string("mechanical")),
1044 );
1045 let definition_context = self.emitter.emit(
1046 "PRODUCT_DEFINITION_CONTEXT",
1047 &format!(
1048 "{},{app_context},{}",
1049 string("part definition"),
1050 string("design")
1051 ),
1052 );
1053
1054 let ir = self.ir;
1055 let products = &ir.model.product_definitions;
1056 let occurrences = &ir.model.occurrences;
1057 let Ok(graph) = AssemblyGraph::new(occurrences) else {
1058 self.loss(
1059 LossKind::TopologyNotTransferred,
1060 Severity::Warning,
1061 "assembly occurrence graph is invalid".into(),
1062 );
1063 return;
1064 };
1065 let occurrence_products = occurrences
1066 .iter()
1067 .filter_map(|occurrence| match &occurrence.prototype {
1068 PrototypeReference::Local { definition } => {
1069 Some((occurrence.id.clone(), definition.clone()))
1070 }
1071 PrototypeReference::External { .. } | PrototypeReference::Unresolved => None,
1072 })
1073 .collect::<HashMap<OccurrenceId, ProductDefinitionId>>();
1074 let mut product_origins = HashMap::<ProductDefinitionId, Ref>::new();
1075 for product in products {
1076 product_origins.insert(
1077 product.id.clone(),
1078 geometry::placement(
1079 &mut self.emitter,
1080 cadmpeg_ir::math::Point3::new(0.0, 0.0, 0.0),
1081 cadmpeg_ir::math::Vector3::new(0.0, 0.0, 1.0),
1082 cadmpeg_ir::math::Vector3::new(1.0, 0.0, 0.0),
1083 ),
1084 );
1085 }
1086 let mut representation_placements = HashMap::<ProductDefinitionId, Vec<Ref>>::new();
1087 let mut occurrence_placements = HashMap::<OccurrenceId, (Ref, Ref)>::new();
1088 for occurrence in occurrences {
1089 let OccurrenceParent::Occurrence { occurrence: parent } = &occurrence.parent else {
1090 continue;
1091 };
1092 let Some(parent_product) = occurrence_products.get(parent) else {
1093 continue;
1094 };
1095 let Some(child_product) = occurrence_products.get(&occurrence.id) else {
1096 continue;
1097 };
1098 let Some(&from) = product_origins.get(child_product) else {
1099 continue;
1100 };
1101 let transform = if occurrence.link_transform.unwrap_or(false) {
1102 occurrence.transform.compose(occurrence.prototype_transform)
1103 } else {
1104 occurrence.transform
1105 };
1106 if !transform.is_proper_rigid() || occurrence.scale != [1.0; 3] {
1107 continue;
1108 }
1109 let rows = transform.rows;
1110 let to = geometry::placement(
1111 &mut self.emitter,
1112 cadmpeg_ir::math::Point3::new(rows[0][3], rows[1][3], rows[2][3]),
1113 cadmpeg_ir::math::Vector3::new(rows[0][2], rows[1][2], rows[2][2]),
1114 cadmpeg_ir::math::Vector3::new(rows[0][0], rows[1][0], rows[2][0]),
1115 );
1116 representation_placements
1117 .entry(parent_product.clone())
1118 .or_default()
1119 .push(to);
1120 occurrence_placements.insert(occurrence.id.clone(), (from, to));
1121 }
1122 let mut definitions = HashMap::<ProductDefinitionId, Ref>::new();
1123 let mut representations = HashMap::<ProductDefinitionId, Ref>::new();
1124 for product in products {
1125 let product_id = product
1126 .part_number
1127 .as_deref()
1128 .or(product.source_name.as_deref())
1129 .unwrap_or(product.id.as_str());
1130 let name = product
1131 .label
1132 .as_deref()
1133 .or(product.source_name.as_deref())
1134 .unwrap_or(product_id);
1135 let product_ref = self.emitter.emit(
1136 "PRODUCT",
1137 &format!(
1138 "{},{},'',({product_context})",
1139 string(product_id),
1140 string(name)
1141 ),
1142 );
1143 let formation = self.emitter.emit(
1144 "PRODUCT_DEFINITION_FORMATION",
1145 &format!("'','',{product_ref}"),
1146 );
1147 let definition = self.emitter.emit(
1148 "PRODUCT_DEFINITION",
1149 &format!("{},'',{formation},{definition_context}", string(product_id)),
1150 );
1151 let shape = self
1152 .emitter
1153 .emit("PRODUCT_DEFINITION_SHAPE", &format!("'','',{definition}"));
1154 self.default_product_definition_shape.get_or_insert(shape);
1155 let mut body_items = product
1156 .bodies
1157 .iter()
1158 .flat_map(|body| {
1159 self.body_item_refs
1160 .get(body.as_str())
1161 .into_iter()
1162 .flatten()
1163 .copied()
1164 })
1165 .collect::<Vec<_>>();
1166 if let Some(origin) = product_origins.get(&product.id) {
1167 body_items.push(*origin);
1168 }
1169 if let Some(placements) = representation_placements.get(&product.id) {
1170 body_items.extend(placements);
1171 }
1172 let representation = self.emitter.emit(
1173 "SHAPE_REPRESENTATION",
1174 &format!("{},{},{context}", string(name), refs(&body_items)),
1175 );
1176 self.emitter.emit(
1177 "SHAPE_DEFINITION_REPRESENTATION",
1178 &format!("{shape},{representation}"),
1179 );
1180 definitions.insert(product.id.clone(), definition);
1181 representations.insert(product.id.clone(), representation);
1182 }
1183
1184 for occurrence in occurrences {
1185 let OccurrenceParent::Occurrence { occurrence: parent } = &occurrence.parent else {
1186 if !is_identity(&occurrence.transform.rows) {
1187 self.loss(
1188 LossKind::BodyTransformNotApplied,
1189 Severity::Warning,
1190 format!(
1191 "root occurrence '{}' has a non-identity placement",
1192 occurrence.id
1193 ),
1194 );
1195 }
1196 continue;
1197 };
1198 let Some(parent_occurrence) = graph.occurrence(parent) else {
1199 self.loss(
1200 LossKind::TopologyNotTransferred,
1201 Severity::Warning,
1202 format!("occurrence '{}' has an unresolved parent", occurrence.id),
1203 );
1204 continue;
1205 };
1206 let Some(parent_product) = occurrence_products.get(&parent_occurrence.id) else {
1207 continue;
1208 };
1209 let Some(child_product) = occurrence_products.get(&occurrence.id) else {
1210 self.loss(
1211 LossKind::TopologyNotTransferred,
1212 Severity::Warning,
1213 format!(
1214 "occurrence '{}' has no local product definition",
1215 occurrence.id
1216 ),
1217 );
1218 continue;
1219 };
1220 let Some((
1221 &parent_definition,
1222 &child_definition,
1223 &parent_representation,
1224 &child_representation,
1225 )) = definitions
1226 .get(parent_product)
1227 .zip(definitions.get(child_product))
1228 .zip(representations.get(parent_product))
1229 .zip(representations.get(child_product))
1230 .map(|(((a, b), c), d)| (a, b, c, d))
1231 else {
1232 continue;
1233 };
1234 let transform = if occurrence.link_transform.unwrap_or(false) {
1235 occurrence.transform.compose(occurrence.prototype_transform)
1236 } else {
1237 occurrence.transform
1238 };
1239 if !transform.is_proper_rigid() || occurrence.scale != [1.0; 3] {
1240 self.loss(
1241 LossKind::BodyTransformNotApplied,
1242 Severity::Warning,
1243 format!("occurrence '{}' placement is not rigid", occurrence.id),
1244 );
1245 continue;
1246 }
1247 let occurrence_name = occurrence.name.as_deref().unwrap_or(occurrence.id.as_str());
1248 let usage = self.emitter.emit(
1249 "NEXT_ASSEMBLY_USAGE_OCCURRENCE",
1250 &format!(
1251 "{},{},'',{parent_definition},{child_definition},$",
1252 string(occurrence.id.as_str()),
1253 string(occurrence_name)
1254 ),
1255 );
1256 let usage_shape = self
1257 .emitter
1258 .emit("PRODUCT_DEFINITION_SHAPE", &format!("'','',{usage}"));
1259 let Some(&(from, to)) = occurrence_placements.get(&occurrence.id) else {
1260 continue;
1261 };
1262 let transform = self
1263 .emitter
1264 .emit("ITEM_DEFINED_TRANSFORMATION", &format!("'','',{from},{to}"));
1265 let relationship = self.emitter.emit_raw(
1266 "REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION",
1267 &format!(
1268 "( REPRESENTATION_RELATIONSHIP('','',{child_representation},{parent_representation}) REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION({transform}) SHAPE_REPRESENTATION_RELATIONSHIP() )"
1269 ),
1270 );
1271 self.emitter.emit(
1272 "CONTEXT_DEPENDENT_SHAPE_REPRESENTATION",
1273 &format!("{relationship},{usage_shape}"),
1274 );
1275 }
1276 }
1277
1278 fn emit_context(&mut self) -> Ref {
1279 let len = self.emit_length_unit();
1280 let angle = self.emit_angle_unit();
1281 let solid = self.emitter.emit_raw(
1282 "SOLID_ANGLE_UNIT",
1283 "( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() )",
1284 );
1285 let unc = self.emitter.emit(
1286 "UNCERTAINTY_MEASURE_WITH_UNIT",
1287 &format!(
1288 "LENGTH_MEASURE({}),{len},{},{}",
1289 real(self.ir.tolerances.linear),
1290 string("distance_accuracy_value"),
1291 string("maximum model space distance")
1292 ),
1293 );
1294 self.emitter.emit_raw(
1295 "GEOMETRIC_REPRESENTATION_CONTEXT",
1296 &format!(
1297 "( GEOMETRIC_REPRESENTATION_CONTEXT(3) \
1298 GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT(({unc})) \
1299 GLOBAL_UNIT_ASSIGNED_CONTEXT(({len},{angle},{solid})) \
1300 REPRESENTATION_CONTEXT('Context','3D') )"
1301 ),
1302 )
1303 }
1304
1305 fn emit_length_unit(&mut self) -> Ref {
1306 if let Some(unit) = self.length_unit {
1307 return unit;
1308 }
1309 let unit = self.emitter.emit_raw(
1310 "LENGTH_UNIT",
1311 "( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) )",
1312 );
1313 self.length_unit = Some(unit);
1314 unit
1315 }
1316
1317 fn emit_angle_unit(&mut self) -> Ref {
1318 if let Some(unit) = self.angle_unit {
1319 return unit;
1320 }
1321 let unit = self.emitter.emit_raw(
1322 "PLANE_ANGLE_UNIT",
1323 "( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) )",
1324 );
1325 self.angle_unit = Some(unit);
1326 unit
1327 }
1328
1329 fn emit_ratio_unit(&mut self) -> Ref {
1330 if let Some(unit) = self.ratio_unit {
1331 return unit;
1332 }
1333 let unit = self
1334 .emitter
1335 .emit_raw("RATIO_UNIT", "( NAMED_UNIT(*) RATIO_UNIT() )");
1336 self.ratio_unit = Some(unit);
1337 unit
1338 }
1339
1340 fn emit_shape_items(&mut self, context: Ref) -> Vec<Ref> {
1343 let mut items = Vec::new();
1344 let ir = self.ir;
1345 for region in &ir.model.regions {
1346 let body_kind = self
1347 .bodies
1348 .get(region.body.as_str())
1349 .map_or(BodyKind::General, |body| body.kind);
1350 if body_kind == BodyKind::Wire {
1351 if let Some(item) = self.emit_wire_region(region) {
1352 let shape_item = self.place_body_item(®ion.body, item, context);
1353 items.push(shape_item);
1354 self.body_shape_refs
1355 .entry(region.body.0.clone())
1356 .or_insert(shape_item);
1357 self.body_item_refs
1358 .entry(region.body.0.clone())
1359 .or_default()
1360 .push(shape_item);
1361 self.body_step_refs
1362 .entry(region.body.0.clone())
1363 .or_insert(item);
1364 }
1365 continue;
1366 }
1367 let closed = body_kind == BodyKind::Solid;
1368 let Some((outer_id, void_ids)) = region.shells.split_first() else {
1369 continue;
1370 };
1371 let Some(outer) = self.emit_shell(outer_id.as_str(), closed) else {
1372 self.topology_relation_loss(
1373 format!("region:{}:outer-shell:{}", region.id, outer_id),
1374 Severity::Error,
1375 format!("region {} has no writable outer shell", region.id),
1376 );
1377 continue;
1378 };
1379 let mut voids = Vec::new();
1380 for sid in void_ids {
1381 if let Some(void) = self.emit_shell(sid.as_str(), closed) {
1382 voids.push(void);
1383 } else {
1384 self.topology_relation_loss(
1385 format!("region:{}:void-shell:{}", region.id, sid),
1386 Severity::Error,
1387 format!(
1388 "region {} omitted void shell {} because it has no writable faces",
1389 region.id, sid
1390 ),
1391 );
1392 }
1393 }
1394 let mut shell_refs = Vec::with_capacity(1 + voids.len());
1395 shell_refs.push(outer);
1396 shell_refs.extend_from_slice(&voids);
1397 let item = if !closed {
1398 self.emitter.emit(
1399 "SHELL_BASED_SURFACE_MODEL",
1400 &format!("'',{}", refs(&shell_refs)),
1401 )
1402 } else if voids.is_empty() {
1403 self.emitter
1404 .emit("MANIFOLD_SOLID_BREP", &format!("'',{outer}"))
1405 } else {
1406 let void_refs: Vec<Ref> = voids
1407 .iter()
1408 .map(|s| {
1409 self.emitter
1410 .emit("ORIENTED_CLOSED_SHELL", &format!("'',*,{s},.F."))
1411 })
1412 .collect();
1413 self.emitter.emit(
1414 "BREP_WITH_VOIDS",
1415 &format!("'',{outer},{}", refs(&void_refs)),
1416 )
1417 };
1418 let shape_item = self.place_body_item(®ion.body, item, context);
1419 items.push(shape_item);
1420 self.body_shape_refs
1421 .entry(region.body.0.clone())
1422 .or_insert(shape_item);
1423 self.body_item_refs
1424 .entry(region.body.0.clone())
1425 .or_default()
1426 .push(shape_item);
1427 self.body_step_refs
1428 .entry(region.body.0.clone())
1429 .or_insert(if closed { item } else { outer });
1430 }
1431 items
1432 }
1433
1434 fn place_body_item(
1435 &mut self,
1436 body_id: &cadmpeg_ir::ids::BodyId,
1437 item: Ref,
1438 context: Ref,
1439 ) -> Ref {
1440 let transform = self
1441 .bodies
1442 .get(body_id.as_str())
1443 .and_then(|body| body.transform);
1444 let Some(transform) = transform.filter(|transform| !is_identity(&transform.rows)) else {
1445 return item;
1446 };
1447 if !is_rigid_transform(&transform.rows) {
1448 self.loss(
1449 LossKind::BodyTransformNotApplied,
1450 Severity::Warning,
1451 format!("body '{body_id}' carries a non-rigid transform"),
1452 );
1453 return item;
1454 }
1455 let origin = geometry::placement(
1456 &mut self.emitter,
1457 cadmpeg_ir::math::Point3::new(0.0, 0.0, 0.0),
1458 cadmpeg_ir::math::Vector3::new(0.0, 0.0, 1.0),
1459 cadmpeg_ir::math::Vector3::new(1.0, 0.0, 0.0),
1460 );
1461 let representation = self.emitter.emit(
1462 "SHAPE_REPRESENTATION",
1463 &format!("'body-local',({item}),{context}"),
1464 );
1465 let map = self
1466 .emitter
1467 .emit("REPRESENTATION_MAP", &format!("{origin},{representation}"));
1468 let rows = transform.rows;
1469 let target = geometry::placement(
1470 &mut self.emitter,
1471 cadmpeg_ir::math::Point3::new(rows[0][3], rows[1][3], rows[2][3]),
1472 cadmpeg_ir::math::Vector3::new(rows[0][2], rows[1][2], rows[2][2]),
1473 cadmpeg_ir::math::Vector3::new(rows[0][0], rows[1][0], rows[2][0]),
1474 );
1475 self.emitter.emit(
1476 "MAPPED_ITEM",
1477 &format!("'cadmpeg body placement',{map},{target}"),
1478 )
1479 }
1480
1481 fn emit_visibility(&mut self) {
1482 if !self.schema.supports_visibility() {
1483 let hidden = self
1484 .ir
1485 .model
1486 .bodies
1487 .iter()
1488 .filter(|body| body.visible == Some(false))
1489 .count();
1490 if hidden != 0 {
1491 self.loss(
1492 LossKind::AttributesNotTransferred,
1493 Severity::Warning,
1494 format!(
1495 "{hidden} hidden body visibility assignment(s) are unsupported by {}",
1496 self.schema.file_schema()
1497 ),
1498 );
1499 }
1500 return;
1501 }
1502 let hidden = self
1503 .ir
1504 .model
1505 .bodies
1506 .iter()
1507 .filter(|body| body.visible == Some(false))
1508 .filter_map(|body| self.body_step_refs.get(body.id.as_str()).copied())
1509 .collect::<Vec<_>>();
1510 if !hidden.is_empty() {
1511 self.emitter.emit("INVISIBILITY", &refs(&hidden));
1512 }
1513 }
1514
1515 fn emit_wire_region(&mut self, region: &cadmpeg_ir::topology::Region) -> Option<Ref> {
1516 let shells = region
1517 .shells
1518 .iter()
1519 .filter_map(|shell_id| self.shells.get(shell_id.as_str()).copied().cloned())
1520 .collect::<Vec<_>>();
1521 let mut connected_sets = Vec::new();
1522 for shell in shells {
1523 if !shell.free_vertices.is_empty() {
1524 self.loss(
1525 LossKind::TopologyNotTransferred,
1526 Severity::Warning,
1527 format!(
1528 "wire shell '{}' has {} free vertex/vertices without an edge-based STEP carrier",
1529 shell.id,
1530 shell.free_vertices.len()
1531 ),
1532 );
1533 }
1534 let edges = shell
1535 .wire_edges
1536 .iter()
1537 .filter_map(|edge| self.emit_edge(edge.as_str()))
1538 .collect::<Vec<_>>();
1539 if !edges.is_empty() {
1540 connected_sets.push(
1541 self.emitter
1542 .emit("CONNECTED_EDGE_SET", &format!("'',{}", refs(&edges))),
1543 );
1544 }
1545 }
1546 if connected_sets.is_empty() {
1547 return None;
1548 }
1549 Some(self.emitter.emit(
1550 "EDGE_BASED_WIREFRAME_MODEL",
1551 &format!("'',{}", refs(&connected_sets)),
1552 ))
1553 }
1554
1555 fn emit_standalone_geometry(&mut self) -> Vec<Ref> {
1556 let surface_ids = self
1557 .ir
1558 .model
1559 .surfaces
1560 .iter()
1561 .filter(|surface| !self.surface_refs.contains_key(surface.id.as_str()))
1562 .map(|surface| surface.id.0.clone())
1563 .collect::<Vec<_>>();
1564 let mut members = Vec::new();
1565 let mut has_surfaces = false;
1566 for surface_id in surface_ids {
1567 if let Some(reference) = self.emit_surface(&surface_id) {
1568 members.push(reference);
1569 has_surfaces = true;
1570 } else {
1571 self.unsupported_standalone_geometry += 1;
1572 }
1573 }
1574 let curve_ids = self
1575 .ir
1576 .model
1577 .curves
1578 .iter()
1579 .filter(|curve| !self.curve_refs.contains_key(curve.id.as_str()))
1580 .map(|curve| curve.id.0.clone())
1581 .collect::<Vec<_>>();
1582 for curve_id in curve_ids {
1583 if self
1584 .curves
1585 .get(curve_id.as_str())
1586 .is_some_and(|curve| matches!(curve.geometry, CurveGeometry::Unknown { .. }))
1587 {
1588 self.unsupported_standalone_geometry += 1;
1589 } else if let Some(reference) = self.emit_curve(&curve_id) {
1590 members.push(reference);
1591 }
1592 }
1593 let point_ids = self
1594 .ir
1595 .model
1596 .points
1597 .iter()
1598 .filter(|point| !self.point_refs.contains_key(point.id.as_str()))
1599 .map(|point| point.id.0.clone())
1600 .collect::<Vec<_>>();
1601 for point_id in point_ids {
1602 let Some(point) = self.points.get(point_id.as_str()).copied() else {
1603 continue;
1604 };
1605 let reference = geometry::point(&mut self.emitter, point.position);
1606 self.point_refs.insert(point_id, reference);
1607 members.push(reference);
1608 }
1609 if members.is_empty() {
1610 Vec::new()
1611 } else {
1612 vec![self.emitter.emit(
1613 if has_surfaces {
1614 "GEOMETRIC_SET"
1615 } else {
1616 "GEOMETRIC_CURVE_SET"
1617 },
1618 &format!("'',{}", refs(&members)),
1619 )]
1620 }
1621 }
1622
1623 fn emit_tessellations(&mut self, context: Ref) {
1624 if self.ir.model.tessellations.is_empty() {
1625 return;
1626 }
1627 if !self.schema.supports_tessellation() {
1628 self.loss(
1629 LossKind::TessellationOmitted,
1630 Severity::Warning,
1631 format!(
1632 "{} tessellation(s) require an AP242 target",
1633 self.ir.model.tessellations.len()
1634 ),
1635 );
1636 return;
1637 }
1638
1639 let ir = self.ir;
1640 let mut representation_items = Vec::new();
1641 for mesh in &ir.model.tessellations {
1642 if mesh.vertices.is_empty()
1643 || mesh.triangles.is_empty()
1644 || mesh
1645 .triangles
1646 .iter()
1647 .flatten()
1648 .any(|index| *index as usize >= mesh.vertices.len())
1649 || (!mesh.normals.is_empty() && mesh.normals.len() != mesh.vertices.len())
1650 {
1651 self.loss(
1652 LossKind::TessellationOmitted,
1653 Severity::Warning,
1654 format!(
1655 "tessellation '{}' has invalid vertex/index/normal cardinality",
1656 mesh.id
1657 ),
1658 );
1659 continue;
1660 }
1661 let coordinates = mesh
1662 .vertices
1663 .iter()
1664 .map(|point| format!("({},{},{})", real(point.x), real(point.y), real(point.z)))
1665 .collect::<Vec<_>>()
1666 .join(",");
1667 let coordinates = self.emitter.emit(
1668 "COORDINATES_LIST",
1669 &format!(
1670 "{}, {},({coordinates})",
1671 string(&mesh.id),
1672 mesh.vertices.len()
1673 ),
1674 );
1675 let normals = if mesh.normals.is_empty() {
1676 "$".to_string()
1677 } else {
1678 format!(
1679 "({})",
1680 mesh.normals
1681 .iter()
1682 .map(|normal| format!(
1683 "({},{},{})",
1684 real(normal.x),
1685 real(normal.y),
1686 real(normal.z)
1687 ))
1688 .collect::<Vec<_>>()
1689 .join(",")
1690 )
1691 };
1692 let point_indices = (1..=mesh.vertices.len())
1693 .map(|index| index.to_string())
1694 .collect::<Vec<_>>()
1695 .join(",");
1696 let linked_body = mesh.body.as_ref().and_then(|body| {
1697 let link = self.body_step_refs.get(body.as_str()).copied()?;
1698 let kind = self.bodies.get(body.as_str())?.kind;
1699 matches!(kind, BodyKind::Solid | BodyKind::Sheet).then_some((kind, link))
1700 });
1701 let item = if let Some((kind, link)) = linked_body {
1702 let triangles = mesh
1703 .triangles
1704 .iter()
1705 .map(|triangle| {
1706 format!(
1707 "({},{},{})",
1708 triangle[0] + 1,
1709 triangle[1] + 1,
1710 triangle[2] + 1
1711 )
1712 })
1713 .collect::<Vec<_>>()
1714 .join(",");
1715 let face = self.emitter.emit(
1716 "TRIANGULATED_FACE",
1717 &format!(
1718 "{},{coordinates},{},{normals},$,({point_indices}),({triangles})",
1719 string(&mesh.id),
1720 mesh.vertices.len()
1721 ),
1722 );
1723 self.emitter.emit(
1724 if kind == BodyKind::Solid {
1725 "TESSELLATED_SOLID"
1726 } else {
1727 "TESSELLATED_SHELL"
1728 },
1729 &format!("{},({face}),{link}", string(&mesh.id)),
1730 )
1731 } else {
1732 let triangles = mesh
1733 .triangles
1734 .iter()
1735 .map(|triangle| {
1736 format!(
1737 "({},{},{})",
1738 triangle[0] + 1,
1739 triangle[1] + 1,
1740 triangle[2] + 1
1741 )
1742 })
1743 .collect::<Vec<_>>()
1744 .join(",");
1745 self.emitter.emit(
1746 "TRIANGULATED_SURFACE_SET",
1747 &format!(
1748 "{},{coordinates},{},{normals},({point_indices}),({triangles})",
1749 string(&mesh.id),
1750 mesh.vertices.len()
1751 ),
1752 )
1753 };
1754 self.tessellation_step_refs.insert(mesh.id.clone(), item);
1755 representation_items.push(item);
1756 }
1757 if !representation_items.is_empty() {
1758 self.emitter.emit(
1759 "TESSELLATED_SHAPE_REPRESENTATION",
1760 &format!("'',{},{context}", refs(&representation_items)),
1761 );
1762 }
1763 }
1764
1765 fn emit_shell(&mut self, shell_id: &str, closed: bool) -> Option<Ref> {
1766 let shell = self.shells.get(shell_id).copied()?;
1767 let face_ids: Vec<String> = shell.faces.iter().map(|f| f.0.clone()).collect();
1768 let mut face_refs = Vec::new();
1769 for fid in &face_ids {
1770 if let Some(r) = self.emit_face(fid) {
1771 face_refs.push(r);
1772 } else {
1773 let outer = self.faces.get(fid.as_str()).is_some_and(|face| {
1774 face.loops.iter().any(|loop_id| {
1775 self.loops
1776 .get(loop_id.as_str())
1777 .is_some_and(|loop_| loop_.boundary_role == LoopBoundaryRole::Outer)
1778 }) || face.loops.first().is_some_and(|loop_id| {
1779 !face.loops.iter().any(|candidate| {
1780 self.loops
1781 .get(candidate.as_str())
1782 .is_some_and(|loop_| loop_.boundary_role == LoopBoundaryRole::Outer)
1783 }) && self.loops.contains_key(loop_id.as_str())
1784 })
1785 });
1786 self.topology_relation_loss(
1787 format!("shell:{shell_id}:face:{fid}"),
1788 if outer {
1789 Severity::Error
1790 } else {
1791 Severity::Warning
1792 },
1793 format!(
1794 "shell {shell_id} omitted face {fid} because the face has no writable topology"
1795 ),
1796 );
1797 }
1798 }
1799 if face_refs.is_empty() {
1800 return None;
1801 }
1802 Some(self.emitter.emit(
1803 if closed { "CLOSED_SHELL" } else { "OPEN_SHELL" },
1804 &format!("'',{}", refs(&face_refs)),
1805 ))
1806 }
1807
1808 fn emit_face(&mut self, face_id: &str) -> Option<Ref> {
1809 let face = self.faces.get(face_id).copied()?;
1810 let surface_id = face.surface.0.clone();
1811 if let Some(surf) = self.surfaces.get(surface_id.as_str()) {
1815 if !geometry::surface_is_supported(&surf.geometry) {
1816 self.unknown_surface_faces.insert(face_id.to_string());
1817 return None;
1818 }
1819 }
1820 let loop_ids: Vec<String> = face.loops.iter().map(|l| l.0.clone()).collect();
1821 let same_sense = matches!(face.sense, Sense::Forward);
1822
1823 let Some(surf_ref) = self.emit_surface(&surface_id) else {
1824 self.unknown_surface_faces.insert(face_id.to_string());
1825 return None;
1826 };
1827
1828 let mut bound_refs = Vec::new();
1829 for (i, lid) in loop_ids.iter().enumerate() {
1830 if let Some(loop_ref) = self.emit_loop(lid) {
1831 let kind = if matches!(
1832 self.loops
1833 .get(lid.as_str())
1834 .map(|loop_| loop_.boundary_role),
1835 Some(LoopBoundaryRole::Outer)
1836 ) || (i == 0
1837 && !loop_ids.iter().any(|id| {
1838 self.loops
1839 .get(id.as_str())
1840 .is_some_and(|loop_| loop_.boundary_role == LoopBoundaryRole::Outer)
1841 })) {
1842 "FACE_OUTER_BOUND"
1843 } else {
1844 "FACE_BOUND"
1845 };
1846 let b = self.emitter.emit(kind, &format!("'',{loop_ref},.T."));
1847 bound_refs.push(b);
1848 } else {
1849 let outer = self
1850 .loops
1851 .get(lid.as_str())
1852 .is_some_and(|loop_| loop_.boundary_role == LoopBoundaryRole::Outer)
1853 || (i == 0
1854 && !loop_ids.iter().any(|id| {
1855 self.loops
1856 .get(id.as_str())
1857 .is_some_and(|loop_| loop_.boundary_role == LoopBoundaryRole::Outer)
1858 }));
1859 self.topology_relation_loss(
1860 format!("face:{face_id}:loop:{lid}"),
1861 if outer {
1862 Severity::Error
1863 } else {
1864 Severity::Warning
1865 },
1866 format!(
1867 "face {face_id} omitted loop {lid} because the loop has no writable topology"
1868 ),
1869 );
1870 }
1871 }
1872 if bound_refs.is_empty() {
1873 self.topology_relation_loss(
1874 format!("face:{face_id}:bound-list"),
1875 Severity::Error,
1876 format!("face {face_id} has no writable bounds"),
1877 );
1878 return None;
1879 }
1880 let flag = if same_sense { ".T." } else { ".F." };
1881 let advanced_face = self.emitter.emit(
1882 "ADVANCED_FACE",
1883 &format!("'',{},{surf_ref},{flag}", refs(&bound_refs)),
1884 );
1885 self.face_step_refs
1886 .insert(face_id.to_string(), advanced_face);
1887 Some(advanced_face)
1888 }
1889
1890 fn emit_loop(&mut self, loop_id: &str) -> Option<Ref> {
1891 let Some(lp) = self.loops.get(loop_id).copied() else {
1892 self.topology_relation_loss(
1893 format!("loop:{loop_id}:record"),
1894 Severity::Warning,
1895 format!("loop {loop_id} was omitted because its record is missing"),
1896 );
1897 return None;
1898 };
1899 if lp.coedges.is_empty() && lp.vertex_uses.len() == 1 {
1900 let vertex_id = lp.vertex_uses[0].vertex.as_str();
1901 let Some(vertex) = self.emit_vertex(vertex_id) else {
1902 self.topology_relation_loss(
1903 format!("loop:{loop_id}:vertex:{vertex_id}"),
1904 Severity::Warning,
1905 format!("loop {loop_id} was omitted because vertex {vertex_id} is missing"),
1906 );
1907 return None;
1908 };
1909 return Some(self.emitter.emit("VERTEX_LOOP", &format!("'',{vertex}")));
1910 }
1911 let coedge_ids: Vec<String> = lp.coedges.iter().map(|c| c.0.clone()).collect();
1912 let mut oe_refs = Vec::new();
1913 for cid in &coedge_ids {
1914 let Some(coedge) = self.coedges.get(cid.as_str()).copied() else {
1915 self.topology_relation_loss(
1916 format!("loop:{loop_id}:coedge:{cid}"),
1917 Severity::Warning,
1918 format!("loop {loop_id} omitted coedge {cid} because its record is missing"),
1919 );
1920 continue;
1921 };
1922 let orientation = matches!(coedge.sense, Sense::Forward);
1923 let Some(edge_ref) = self.emit_edge(coedge.edge.as_str()) else {
1924 self.topology_relation_loss(
1925 format!("loop:{loop_id}:edge:{}", coedge.edge),
1926 Severity::Warning,
1927 format!(
1928 "loop {loop_id} omitted edge {} because the edge is not writable",
1929 coedge.edge
1930 ),
1931 );
1932 continue;
1933 };
1934 let flag = if orientation { ".T." } else { ".F." };
1935 let oe = self
1936 .emitter
1937 .emit("ORIENTED_EDGE", &format!("'',*,*,{edge_ref},{flag}"));
1938 oe_refs.push(oe);
1939 }
1940 if oe_refs.is_empty() {
1941 self.topology_relation_loss(
1942 format!("loop:{loop_id}:edge-list"),
1943 Severity::Warning,
1944 format!("loop {loop_id} has no writable edges"),
1945 );
1946 return None;
1947 }
1948 Some(
1949 self.emitter
1950 .emit("EDGE_LOOP", &format!("'',{}", refs(&oe_refs))),
1951 )
1952 }
1953
1954 fn emit_edge(&mut self, edge_id: &str) -> Option<Ref> {
1955 if let Some(r) = self.edge_refs.get(edge_id) {
1956 return Some(*r);
1957 }
1958 let Some(edge) = self.edges.get(edge_id).copied() else {
1959 self.topology_relation_loss(
1960 format!("edge:{edge_id}:record"),
1961 Severity::Warning,
1962 format!("edge {edge_id} was omitted because its record is missing"),
1963 );
1964 return None;
1965 };
1966 let Some(v1) = self.emit_vertex(edge.start.as_str()) else {
1967 self.topology_relation_loss(
1968 format!("edge:{edge_id}:start-vertex:{}", edge.start),
1969 Severity::Warning,
1970 format!(
1971 "edge {edge_id} was omitted because start vertex {} is missing",
1972 edge.start
1973 ),
1974 );
1975 return None;
1976 };
1977 let Some(v2) = self.emit_vertex(edge.end.as_str()) else {
1978 self.topology_relation_loss(
1979 format!("edge:{edge_id}:end-vertex:{}", edge.end),
1980 Severity::Warning,
1981 format!(
1982 "edge {edge_id} was omitted because end vertex {} is missing",
1983 edge.end
1984 ),
1985 );
1986 return None;
1987 };
1988 let Some(curve_id) = &edge.curve else {
1989 self.curveless_edges.insert(edge_id.to_string());
1990 self.topology_relation_loss(
1991 format!("edge:{edge_id}:curve"),
1992 Severity::Warning,
1993 format!("edge {edge_id} was omitted because it has no 3D curve"),
1994 );
1995 return None;
1996 };
1997 if self
1998 .curves
1999 .get(curve_id.as_str())
2000 .is_some_and(|curve| !geometry::curve_is_supported(&curve.geometry))
2001 {
2002 self.curveless_edges.insert(edge_id.to_string());
2003 self.topology_relation_loss(
2004 format!("edge:{edge_id}:unsupported-curve:{curve_id}"),
2005 Severity::Warning,
2006 format!("edge {edge_id} was omitted because curve {curve_id} is unsupported"),
2007 );
2008 return None;
2009 }
2010 let Some(basis_curve) = self.emit_curve(curve_id.as_str()) else {
2011 self.topology_relation_loss(
2012 format!("edge:{edge_id}:curve:{curve_id}"),
2013 Severity::Warning,
2014 format!("edge {edge_id} was omitted because curve {curve_id} is not writable"),
2015 );
2016 return None;
2017 };
2018 let associated = self.edge_coedges.get(edge_id).cloned().unwrap_or_default();
2019 let mut pcurve_refs = Vec::new();
2020 for (pcurve_id, surface_id) in associated {
2021 if let Some(pcurve) = self.emit_pcurve(pcurve_id, surface_id) {
2022 pcurve_refs.push(pcurve);
2023 }
2024 }
2025 let curve_ref = if pcurve_refs.is_empty() {
2026 basis_curve
2027 } else {
2028 self.emitter.emit(
2029 "SURFACE_CURVE",
2030 &format!("'',{basis_curve},{},.CURVE_3D.", refs(&pcurve_refs)),
2031 )
2032 };
2033 let r = self
2036 .emitter
2037 .emit("EDGE_CURVE", &format!("'',{v1},{v2},{curve_ref},.T."));
2038 self.edge_refs.insert(edge_id.to_string(), r);
2039 Some(r)
2040 }
2041
2042 fn emit_pcurve(&mut self, pcurve_id: &str, surface_id: &str) -> Option<Ref> {
2043 let pcurve = self.pcurves.get(pcurve_id).copied()?;
2044 let surface = self.emit_surface(surface_id)?;
2045 let curve = geometry::pcurve(&mut self.emitter, &pcurve.geometry)?;
2046 let context = if let Some(context) = self.pcurve_context {
2047 context
2048 } else {
2049 let context = self.emitter.emit_raw(
2050 "GEOMETRIC_REPRESENTATION_CONTEXT",
2051 "( GEOMETRIC_REPRESENTATION_CONTEXT(2) PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('uv','2D') )",
2052 );
2053 self.pcurve_context = Some(context);
2054 context
2055 };
2056 let representation = self.emitter.emit(
2057 "DEFINITIONAL_REPRESENTATION",
2058 &format!("'',({curve}),{context}"),
2059 );
2060 Some(
2061 self.emitter
2062 .emit("PCURVE", &format!("'',{surface},{representation}")),
2063 )
2064 }
2065
2066 fn emit_vertex(&mut self, vertex_id: &str) -> Option<Ref> {
2067 if let Some(r) = self.vertex_refs.get(vertex_id) {
2068 return Some(*r);
2069 }
2070 let vertex = self.vertices.get(vertex_id).copied()?;
2071 let pt = self.points.get(vertex.point.as_str()).copied()?;
2072 let cp = geometry::point(&mut self.emitter, pt.position);
2073 self.point_refs.insert(vertex.point.0.clone(), cp);
2074 let r = self.emitter.emit("VERTEX_POINT", &format!("'',{cp}"));
2075 self.vertex_refs.insert(vertex_id.to_string(), r);
2076 Some(r)
2077 }
2078
2079 fn emit_surface(&mut self, surface_id: &str) -> Option<Ref> {
2080 if let Some(r) = self.surface_refs.get(surface_id) {
2081 return Some(*r);
2082 }
2083 if self.geometry_emission_depth >= 256
2084 || !self.active_surfaces.insert(surface_id.to_string())
2085 {
2086 return None;
2087 }
2088 self.geometry_emission_depth += 1;
2089 let result = (|| {
2090 let surf = self.surfaces.get(surface_id).copied()?;
2091 let procedural = self
2092 .procedural_surfaces
2093 .get(surface_id)
2094 .map(|procedural| (procedural.id.0.clone(), procedural.definition.clone()));
2095 let emitted = procedural.and_then(|(id, definition)| {
2096 self.emit_procedural_surface(&surf.geometry, &definition)
2097 .map(|reference| (id, reference))
2098 });
2099 let r = if let Some((id, reference)) = emitted {
2100 self.written_procedural_surfaces.insert(id);
2101 reference
2102 } else if !geometry::surface_is_supported(&surf.geometry) {
2103 return None;
2104 } else {
2105 geometry::surface(&mut self.emitter, &surf.geometry)
2106 };
2107 Some(r)
2108 })();
2109 self.active_surfaces.remove(surface_id);
2110 self.geometry_emission_depth -= 1;
2111 if let Some(r) = result {
2112 self.surface_refs.insert(surface_id.to_string(), r);
2113 }
2114 result
2115 }
2116
2117 fn emit_procedural_surface(
2118 &mut self,
2119 solved: &SurfaceGeometry,
2120 definition: &ProceduralSurfaceDefinition,
2121 ) -> Option<Ref> {
2122 let logical = |value: Option<bool>| match value {
2123 Some(true) => ".T.",
2124 Some(false) => ".F.",
2125 None => ".U.",
2126 };
2127 match definition {
2128 ProceduralSurfaceDefinition::LinearSweep {
2129 directrix,
2130 direction,
2131 } => {
2132 let directrix = self.emit_curve(directrix.as_str())?;
2133 let direction_ref = geometry::direction(&mut self.emitter, *direction);
2134 let vector = self.emitter.emit(
2135 "VECTOR",
2136 &format!("'',{direction_ref},{}", real(direction.norm())),
2137 );
2138 Some(self.emitter.emit(
2139 "SURFACE_OF_LINEAR_EXTRUSION",
2140 &format!("'',{directrix},{vector}"),
2141 ))
2142 }
2143 ProceduralSurfaceDefinition::AxisRevolution {
2144 directrix,
2145 axis_origin,
2146 axis_direction,
2147 } => {
2148 let directrix = self.emit_curve(directrix.as_str())?;
2149 let origin = geometry::point(&mut self.emitter, *axis_origin);
2150 let direction = geometry::direction(&mut self.emitter, *axis_direction);
2151 let axis = self
2152 .emitter
2153 .emit("AXIS1_PLACEMENT", &format!("'',{origin},{direction}"));
2154 Some(
2155 self.emitter
2156 .emit("SURFACE_OF_REVOLUTION", &format!("'',{directrix},{axis}")),
2157 )
2158 }
2159 ProceduralSurfaceDefinition::ParallelOffset {
2160 support,
2161 distance,
2162 self_intersect,
2163 } => {
2164 let support = self.emit_surface(support.as_str())?;
2165 Some(self.emitter.emit(
2166 "OFFSET_SURFACE",
2167 &format!(
2168 "'',{support},{},{}",
2169 real(*distance),
2170 logical(*self_intersect)
2171 ),
2172 ))
2173 }
2174 ProceduralSurfaceDefinition::DegenerateTorus { select_outer } => {
2175 let SurfaceGeometry::Torus {
2176 center,
2177 axis,
2178 ref_direction,
2179 major_radius,
2180 minor_radius,
2181 } = solved
2182 else {
2183 return None;
2184 };
2185 let placement =
2186 geometry::placement(&mut self.emitter, *center, *axis, *ref_direction);
2187 Some(self.emitter.emit(
2188 "DEGENERATE_TOROIDAL_SURFACE",
2189 &format!(
2190 "'',{placement},{},{},{}",
2191 real(major_radius.abs()),
2192 real(minor_radius.abs()),
2193 if *select_outer { ".T." } else { ".F." }
2194 ),
2195 ))
2196 }
2197 _ => None,
2198 }
2199 }
2200
2201 fn emit_curve(&mut self, curve_id: &str) -> Option<Ref> {
2202 if let Some(r) = self.curve_refs.get(curve_id) {
2203 return Some(*r);
2204 }
2205 if self.geometry_emission_depth >= 256 || !self.active_curves.insert(curve_id.to_string()) {
2206 return None;
2207 }
2208 self.geometry_emission_depth += 1;
2209 let result = (|| {
2210 let geometry = self.curves.get(curve_id)?.geometry.clone();
2211 let procedural = self
2212 .procedural_curves
2213 .get(curve_id)
2214 .map(|procedural| (procedural.id.0.clone(), procedural.definition.clone()));
2215 let emitted = procedural.and_then(|(id, definition)| {
2216 self.emit_procedural_curve(&definition)
2217 .map(|reference| (id, reference))
2218 });
2219 let r = if let Some((id, reference)) = emitted {
2220 self.written_procedural_curves.insert(id);
2221 reference
2222 } else if let CurveGeometry::Composite {
2223 segments,
2224 self_intersect,
2225 } = &geometry
2226 {
2227 let mut segment_refs = Vec::with_capacity(segments.len());
2228 for segment in segments {
2229 let curve = self.emit_curve(segment.curve.as_str())?;
2230 let transition = match segment.transition {
2231 cadmpeg_ir::geometry::CompositeCurveTransition::Discontinuous => {
2232 ".DISCONTINUOUS."
2233 }
2234 cadmpeg_ir::geometry::CompositeCurveTransition::Continuous => ".CONTINUOUS.",
2235 cadmpeg_ir::geometry::CompositeCurveTransition::ContSameGradient => {
2236 ".CONTSAMEGRADIENT."
2237 }
2238 cadmpeg_ir::geometry::CompositeCurveTransition::ContSameGradientSameCurvature => {
2239 ".CONTSAMEGRADIENTSAMECURVATURE."
2240 }
2241 };
2242 segment_refs.push(self.emitter.emit(
2243 "COMPOSITE_CURVE_SEGMENT",
2244 &format!(
2245 "{transition},{},{curve}",
2246 if segment.same_sense { ".T." } else { ".F." }
2247 ),
2248 ));
2249 }
2250 self.emitter.emit(
2251 "COMPOSITE_CURVE",
2252 &format!(
2253 "'',{},{}",
2254 refs(&segment_refs),
2255 match self_intersect {
2256 Some(true) => ".T.",
2257 Some(false) => ".F.",
2258 None => ".U.",
2259 }
2260 ),
2261 )
2262 } else if !geometry::curve_is_supported(&geometry) {
2263 return None;
2264 } else {
2265 geometry::curve(&mut self.emitter, &geometry)
2266 };
2267 Some(r)
2268 })();
2269 self.active_curves.remove(curve_id);
2270 self.geometry_emission_depth -= 1;
2271 if let Some(r) = result {
2272 self.curve_refs.insert(curve_id.to_string(), r);
2273 }
2274 result
2275 }
2276
2277 fn emit_procedural_curve(&mut self, definition: &ProceduralCurveDefinition) -> Option<Ref> {
2278 match definition {
2279 ProceduralCurveDefinition::Subset {
2280 source,
2281 parameter_range: [start, end],
2282 } => {
2283 let source = self.emit_curve(source.as_str())?;
2284 Some(self.emitter.emit(
2285 "TRIMMED_CURVE",
2286 &format!(
2287 "'',{source},(PARAMETER_VALUE({})),(PARAMETER_VALUE({})),.T.,.PARAMETER.",
2288 real(*start),
2289 real(*end)
2290 ),
2291 ))
2292 }
2293 ProceduralCurveDefinition::SpatialOffset {
2294 source,
2295 distance,
2296 reference_direction,
2297 self_intersect,
2298 } => {
2299 let source = self.emit_curve(source.as_str())?;
2300 let direction = geometry::direction(&mut self.emitter, *reference_direction);
2301 let self_intersect = match self_intersect {
2302 Some(true) => ".T.",
2303 Some(false) => ".F.",
2304 None => ".U.",
2305 };
2306 Some(self.emitter.emit(
2307 "OFFSET_CURVE_3D",
2308 &format!(
2309 "'',{source},{},{self_intersect},{direction}",
2310 real(*distance)
2311 ),
2312 ))
2313 }
2314 _ => None,
2315 }
2316 }
2317
2318 fn emit_pmi(&mut self, context: Ref) {
2319 use cadmpeg_ir::pmi::{DimensionKind, GeometricToleranceKind, PmiDefinition, PmiTarget};
2320
2321 if self.ir.model.pmi.is_empty() || !self.schema.supports_semantic_pmi() {
2322 return;
2323 }
2324 let annotations = self.ir.model.pmi.clone();
2325 let Some(pds) = self.default_product_definition_shape else {
2326 return;
2327 };
2328 let mut annotation_refs = HashMap::new();
2329 let mut aspects = HashMap::<String, Ref>::new();
2330 for annotation in &annotations {
2331 for target in &annotation.targets {
2332 let PmiTarget::ShapeAspect { source_id } = target else {
2333 continue;
2334 };
2335 aspects.entry(source_id.clone()).or_insert_with(|| {
2336 self.emitter.emit(
2337 "SHAPE_ASPECT",
2338 &format!("{},'',{pds},.T.", string(source_id)),
2339 )
2340 });
2341 }
2342 }
2343 let fallback_aspect = self
2344 .emitter
2345 .emit("SHAPE_ASPECT", &format!("'PMI target','',{pds},.T."));
2346 let target_ref = |annotation: &cadmpeg_ir::PmiAnnotation| {
2347 annotation.targets.iter().find_map(|target| {
2348 if let PmiTarget::ShapeAspect { source_id } = target {
2349 aspects.get(source_id).copied()
2350 } else {
2351 None
2352 }
2353 })
2354 };
2355 let targets_exact = |annotation: &cadmpeg_ir::PmiAnnotation| {
2356 annotation
2357 .targets
2358 .iter()
2359 .all(|target| matches!(target, PmiTarget::ShapeAspect { .. }))
2360 };
2361
2362 for annotation in &annotations {
2363 if let PmiDefinition::Datum { identification } = &annotation.definition {
2364 let datum = self.emitter.emit(
2365 "DATUM",
2366 &format!(
2367 "{},$,{pds},.F.,{}",
2368 string(annotation.name.as_deref().unwrap_or("")),
2369 string(identification)
2370 ),
2371 );
2372 annotation_refs.insert(annotation.id.clone(), datum);
2373 self.written_pmi += usize::from(targets_exact(annotation));
2374 }
2375 }
2376 for annotation in &annotations {
2377 if let PmiDefinition::DatumSystem { references } = &annotation.definition {
2378 let mut groups = BTreeMap::<(u32, Option<u32>), Vec<_>>::new();
2379 for reference in references {
2380 groups
2381 .entry((reference.precedence, reference.common_group))
2382 .or_default()
2383 .push(reference);
2384 }
2385 let compartments = groups
2386 .values()
2387 .filter_map(|group| {
2388 let datum_refs = group
2389 .iter()
2390 .map(|reference| annotation_refs.get(&reference.datum).copied())
2391 .collect::<Option<Vec<_>>>()?;
2392 if group[0].common_group.is_none() && group.len() != 1 {
2393 return None;
2394 }
2395 let (datum, modifiers) = if group[0].common_group.is_some() {
2396 let elements = group
2397 .iter()
2398 .zip(datum_refs)
2399 .map(|(reference, datum)| {
2400 let modifiers =
2401 self.emit_datum_modifiers(&reference.modifiers)?;
2402 Some(self.emitter.emit(
2403 "DATUM_REFERENCE_ELEMENT",
2404 &format!("'',$,{pds},.F.,{datum},({modifiers})"),
2405 ))
2406 })
2407 .collect::<Option<Vec<_>>>()?;
2408 (
2409 format!("COMMON_DATUM_LIST({})", refs(&elements)),
2410 String::new(),
2411 )
2412 } else {
2413 (
2414 datum_refs[0].to_string(),
2415 self.emit_datum_modifiers(&group[0].modifiers)?,
2416 )
2417 };
2418 Some(self.emitter.emit(
2419 "DATUM_REFERENCE_COMPARTMENT",
2420 &format!("'',$,{pds},.F.,{datum},({modifiers})"),
2421 ))
2422 })
2423 .collect::<Vec<_>>();
2424 let complete = compartments.len() == groups.len();
2425 if compartments.is_empty() {
2426 continue;
2427 }
2428 let system = self.emitter.emit(
2429 "DATUM_SYSTEM",
2430 &format!(
2431 "{},'',{pds},.F.,{}",
2432 string(annotation.name.as_deref().unwrap_or("")),
2433 refs(&compartments)
2434 ),
2435 );
2436 annotation_refs.insert(annotation.id.clone(), system);
2437 self.written_pmi += usize::from(targets_exact(annotation) && complete);
2438 }
2439 }
2440 for annotation in &annotations {
2441 match &annotation.definition {
2442 PmiDefinition::Dimension {
2443 dimension,
2444 nominal,
2445 lower_deviation,
2446 upper_deviation,
2447 limits_and_fits,
2448 } => {
2449 let aspect = target_ref(annotation).unwrap_or(fallback_aspect);
2450 let name = annotation.name.as_deref().unwrap_or("");
2451 let (entity, kind_exact) = match dimension {
2452 DimensionKind::Size => ("DIMENSIONAL_SIZE", true),
2453 DimensionKind::Location => ("DIMENSIONAL_LOCATION", true),
2454 DimensionKind::Angular => ("ANGULAR_SIZE", true),
2455 DimensionKind::Diameter | DimensionKind::Radius => {
2460 ("DIMENSIONAL_SIZE", true)
2461 }
2462 DimensionKind::Other(_) => ("DIMENSIONAL_SIZE", false),
2463 };
2464 let characteristic_name = match dimension {
2465 DimensionKind::Diameter => "diameter",
2466 DimensionKind::Radius => "radius",
2467 _ => name,
2468 };
2469 let parameters = match dimension {
2470 DimensionKind::Location => {
2471 format!("{},$,{aspect},{aspect}", string(characteristic_name))
2472 }
2473 DimensionKind::Angular => {
2474 format!("{aspect},{},.SMALL.", string(characteristic_name))
2475 }
2476 _ => format!("{aspect},{}", string(characteristic_name)),
2477 };
2478 let characteristic = self.emitter.emit(entity, ¶meters);
2479 if let Some(value) = nominal {
2480 let measure = self.emit_pmi_measure_representation_item(*value, name);
2481 let representation = self.emitter.emit(
2482 "SHAPE_DIMENSION_REPRESENTATION",
2483 &format!("'',({measure}),{context}"),
2484 );
2485 self.emitter.emit(
2486 "DIMENSIONAL_CHARACTERISTIC_REPRESENTATION",
2487 &format!("{characteristic},{representation}"),
2488 );
2489 }
2490 if let (Some(lower), Some(upper)) = (lower_deviation, upper_deviation) {
2491 let lower = self.emit_pmi_measure(*lower);
2492 let upper = self.emit_pmi_measure(*upper);
2493 let tolerance = self
2494 .emitter
2495 .emit("TOLERANCE_VALUE", &format!("{lower},{upper}"));
2496 self.emitter.emit(
2497 "PLUS_MINUS_TOLERANCE",
2498 &format!("{tolerance},{characteristic}"),
2499 );
2500 }
2501 if let Some(fit) = limits_and_fits {
2502 let fit = self.emitter.emit(
2503 "LIMITS_AND_FITS",
2504 &format!(
2505 "{},{},{},{}",
2506 string(&fit.form_variance),
2507 string(&fit.zone_variance),
2508 string(&fit.grade),
2509 string(&fit.source)
2510 ),
2511 );
2512 self.emitter
2513 .emit("PLUS_MINUS_TOLERANCE", &format!("{fit},{characteristic}"));
2514 }
2515 annotation_refs.insert(annotation.id.clone(), characteristic);
2516 let deviations_exact = lower_deviation.is_some() == upper_deviation.is_some();
2517 self.written_pmi +=
2518 usize::from(targets_exact(annotation) && deviations_exact && kind_exact);
2519 }
2520 PmiDefinition::GeometricTolerance {
2521 tolerance,
2522 magnitude,
2523 datum_system,
2524 } => {
2525 let kind_exact = !matches!(tolerance, GeometricToleranceKind::Other(value) if value != "geometric_tolerance");
2526 let entity = match tolerance {
2527 GeometricToleranceKind::Straightness => "STRAIGHTNESS_TOLERANCE",
2528 GeometricToleranceKind::Flatness => "FLATNESS_TOLERANCE",
2529 GeometricToleranceKind::Roundness => "ROUNDNESS_TOLERANCE",
2530 GeometricToleranceKind::Cylindricity => "CYLINDRICITY_TOLERANCE",
2531 GeometricToleranceKind::LineProfile => "LINE_PROFILE_TOLERANCE",
2532 GeometricToleranceKind::SurfaceProfile => "SURFACE_PROFILE_TOLERANCE",
2533 GeometricToleranceKind::Angularity => "ANGULARITY_TOLERANCE",
2534 GeometricToleranceKind::Perpendicularity => "PERPENDICULARITY_TOLERANCE",
2535 GeometricToleranceKind::Parallelism => "PARALLELISM_TOLERANCE",
2536 GeometricToleranceKind::Position => "POSITION_TOLERANCE",
2537 GeometricToleranceKind::Concentricity => "CONCENTRICITY_TOLERANCE",
2538 GeometricToleranceKind::Symmetry => "SYMMETRY_TOLERANCE",
2539 GeometricToleranceKind::CircularRunout => "CIRCULAR_RUNOUT_TOLERANCE",
2540 GeometricToleranceKind::TotalRunout => "TOTAL_RUNOUT_TOLERANCE",
2541 GeometricToleranceKind::Other(_) => continue,
2542 };
2543 let measure = self.emit_pmi_measure(*magnitude);
2544 let aspect = target_ref(annotation).unwrap_or(fallback_aspect);
2545 if datum_system.is_some() {
2551 continue;
2552 }
2553 let tolerance_ref = self.emitter.emit(
2554 entity,
2555 &format!(
2556 "{},'',{measure},{aspect}",
2557 string(annotation.name.as_deref().unwrap_or(""))
2558 ),
2559 );
2560 annotation_refs.insert(annotation.id.clone(), tolerance_ref);
2561 self.written_pmi += usize::from(targets_exact(annotation) && kind_exact);
2562 }
2563 PmiDefinition::Datum { .. }
2564 | PmiDefinition::DatumSystem { .. }
2565 | PmiDefinition::Presentation { .. } => {}
2566 }
2567 }
2568 let mut presentation_items = Vec::new();
2569 let mut presentation_semantics = Vec::new();
2570 for annotation in &annotations {
2571 let PmiDefinition::Presentation {
2572 text,
2573 placement,
2574 semantics,
2575 } = &annotation.definition
2576 else {
2577 continue;
2578 };
2579 let (Some(text), Some(placement)) = (text.as_deref(), placement.as_ref()) else {
2580 continue;
2581 };
2582 if !annotation.targets.is_empty() || !is_rigid_transform(&placement.rows) {
2583 continue;
2584 }
2585 let rows = placement.rows;
2586 let placement = geometry::placement(
2587 &mut self.emitter,
2588 cadmpeg_ir::math::Point3::new(rows[0][3], rows[1][3], rows[2][3]),
2589 cadmpeg_ir::math::Vector3::new(rows[0][2], rows[1][2], rows[2][2]),
2590 cadmpeg_ir::math::Vector3::new(rows[0][0], rows[1][0], rows[2][0]),
2591 );
2592 let font_source = self.emitter.emit("EXTERNAL_SOURCE", "'ISO 3098'");
2593 let font = self.emitter.emit(
2594 "EXTERNALLY_DEFINED_TEXT_FONT",
2595 &format!("IDENTIFIER('ISO 3098'),{font_source}"),
2596 );
2597 let literal = self.emitter.emit(
2598 "TEXT_LITERAL",
2599 &format!("{},{placement},'left',.RIGHT.,{font}", string(text)),
2600 );
2601 let semantic_refs = semantics
2602 .iter()
2603 .filter_map(|semantic| annotation_refs.get(semantic).copied())
2604 .collect::<Vec<_>>();
2605 if semantic_refs.len() != semantics.len() {
2606 continue;
2607 }
2608 let style = self
2609 .emitter
2610 .emit("PRESENTATION_STYLE_ASSIGNMENT", "(.NULL.)");
2611 let occurrence = self.emitter.emit(
2612 "ANNOTATION_TEXT_OCCURRENCE",
2613 &format!(
2614 "{},{},{literal}",
2615 string(annotation.name.as_deref().unwrap_or("")),
2616 refs(&[style])
2617 ),
2618 );
2619 presentation_items.push(occurrence);
2620 presentation_semantics.push((occurrence, semantic_refs));
2621 annotation_refs.insert(annotation.id.clone(), occurrence);
2622 self.written_pmi += 1;
2623 }
2624 if !presentation_items.is_empty() {
2625 let model = self.emitter.emit(
2626 "DRAUGHTING_MODEL",
2627 &format!(
2628 "'PMI presentation',{}, {context}",
2629 refs(&presentation_items)
2630 ),
2631 );
2632 for (occurrence, semantics) in presentation_semantics {
2633 for semantic in semantics {
2634 self.emitter.emit(
2635 "DRAUGHTING_MODEL_ITEM_ASSOCIATION",
2636 &format!("'','',{semantic},{model},{occurrence}"),
2637 );
2638 }
2639 }
2640 }
2641 }
2642
2643 fn emit_datum_modifiers(&mut self, source: &[String]) -> Option<String> {
2644 let mut modifiers = Vec::with_capacity(source.len());
2645 for modifier in source {
2646 if let Some((kind, value)) = modifier.split_once(':') {
2647 let value = value.parse::<f64>().ok()?;
2648 let measure = self.emit_pmi_measure(cadmpeg_ir::PmiValue {
2649 value,
2650 quantity: cadmpeg_ir::PmiQuantity::Length,
2651 });
2652 modifiers.push(
2653 self.emitter
2654 .emit(
2655 "DATUM_REFERENCE_MODIFIER_WITH_VALUE",
2656 &format!(".{}.,{measure}", kind.to_ascii_uppercase()),
2657 )
2658 .to_string(),
2659 );
2660 } else {
2661 modifiers.push(format!(".{}.", modifier.to_ascii_uppercase()));
2662 }
2663 }
2664 Some(modifiers.join(","))
2665 }
2666
2667 fn emit_pmi_measure(&mut self, value: cadmpeg_ir::PmiValue) -> Ref {
2668 use cadmpeg_ir::pmi::PmiQuantity;
2669 let (entity, typed, unit) = match value.quantity {
2670 PmiQuantity::Length => (
2671 "LENGTH_MEASURE_WITH_UNIT",
2672 "LENGTH_MEASURE",
2673 self.emit_length_unit(),
2674 ),
2675 PmiQuantity::Angle => (
2676 "PLANE_ANGLE_MEASURE_WITH_UNIT",
2677 "PLANE_ANGLE_MEASURE",
2678 self.emit_angle_unit(),
2679 ),
2680 PmiQuantity::Ratio => ("MEASURE_WITH_UNIT", "RATIO_MEASURE", self.emit_ratio_unit()),
2681 };
2682 self.emitter
2683 .emit(entity, &format!("{typed}({}),{unit}", real(value.value)))
2684 }
2685
2686 fn emit_pmi_measure_representation_item(
2687 &mut self,
2688 value: cadmpeg_ir::PmiValue,
2689 name: &str,
2690 ) -> Ref {
2691 use cadmpeg_ir::pmi::PmiQuantity;
2692 let (typed, unit) = match value.quantity {
2693 PmiQuantity::Length => ("LENGTH_MEASURE", self.emit_length_unit()),
2694 PmiQuantity::Angle => ("PLANE_ANGLE_MEASURE", self.emit_angle_unit()),
2695 PmiQuantity::Ratio => ("RATIO_MEASURE", self.emit_ratio_unit()),
2696 };
2697 self.emitter.emit(
2698 "MEASURE_REPRESENTATION_ITEM",
2699 &format!("{},{typed}({}),{unit}", string(name), real(value.value)),
2700 )
2701 }
2702
2703 fn note_unrepresented(&mut self) {
2704 let nonstandard_analytic_surfaces = self
2705 .ir
2706 .model
2707 .surfaces
2708 .iter()
2709 .filter(|surface| match &surface.geometry {
2710 SurfaceGeometry::Sphere { radius, .. } => *radius < 0.0,
2711 SurfaceGeometry::Torus {
2712 major_radius,
2713 minor_radius,
2714 ..
2715 } => {
2716 *major_radius < 0.0
2717 || *minor_radius < 0.0
2718 || (minor_radius.abs() > major_radius.abs()
2719 && !self.ir.model.procedural_surfaces.iter().any(|procedural| {
2720 procedural.surface == surface.id
2721 && self.written_procedural_surfaces.contains(&procedural.id.0)
2722 && matches!(
2723 procedural.definition,
2724 ProceduralSurfaceDefinition::DegenerateTorus { .. }
2725 )
2726 }))
2727 }
2728 _ => false,
2729 })
2730 .count();
2731 if nonstandard_analytic_surfaces > 0 {
2732 self.loss(
2733 LossKind::AnalyticSurfaceNormalized,
2734 Severity::Warning,
2735 format!(
2736 "{nonstandard_analytic_surfaces} signed or self-intersecting analytic \
2737 surface(s) were normalized to positive STEP radii"
2738 ),
2739 );
2740 }
2741 let elliptical_cones = self
2742 .ir
2743 .model
2744 .surfaces
2745 .iter()
2746 .filter(|surface| {
2747 matches!(
2748 surface.geometry,
2749 SurfaceGeometry::Cone { ratio, .. } if ratio != 1.0
2750 )
2751 })
2752 .count();
2753 if elliptical_cones > 0 {
2754 self.loss(
2755 LossKind::EllipticalConeReduced,
2756 Severity::Warning,
2757 format!(
2758 "{elliptical_cones} elliptical cone surface(s) were reduced to circular STEP CONICAL_SURFACE carriers"
2759 ),
2760 );
2761 }
2762 if !self.curveless_edges.is_empty() {
2763 self.omit(
2764 LossKind::CurvelessEdgeOmitted,
2765 Severity::Warning,
2766 format!(
2767 "{} edge(s) have no typed 3D curve or carry a STEP-unsupported transform and were omitted from \
2768 their edge loops (STEP EDGE_CURVE requires a 3D curve)",
2769 self.curveless_edges.len()
2770 ),
2771 );
2772 }
2773 if !self.unknown_surface_faces.is_empty() {
2774 self.omit(
2775 LossKind::UnknownSurfaceFaceOmitted,
2776 Severity::Warning,
2777 format!(
2778 "{} face(s) rest on an unknown or STEP-unsupported surface and were omitted \
2779 from the STEP shell (an ADVANCED_FACE requires a surface); their \
2780 topology remains in the IR",
2781 self.unknown_surface_faces.len()
2782 ),
2783 );
2784 }
2785 if self.unsupported_standalone_geometry > 0 {
2786 self.loss(
2787 LossKind::GeometryNotTransferred,
2788 Severity::Warning,
2789 format!(
2790 "{} standalone unknown geometry carrier(s) were not written",
2791 self.unsupported_standalone_geometry
2792 ),
2793 );
2794 }
2795 let missing_pcurve_count = self
2796 .ir
2797 .model
2798 .coedges
2799 .iter()
2800 .flat_map(|coedge| &coedge.pcurves)
2801 .filter(|use_| !self.pcurves.contains_key(use_.pcurve.as_str()))
2802 .count();
2803 if missing_pcurve_count > 0 {
2804 self.omit(
2805 LossKind::PcurveOmitted,
2806 Severity::Warning,
2807 format!(
2808 "{missing_pcurve_count} coedge pcurve reference(s) have no geometry and were not written"
2809 ),
2810 );
2811 }
2812 let reduced_pcurve_count = self
2813 .ir
2814 .model
2815 .coedges
2816 .iter()
2817 .flat_map(|coedge| &coedge.pcurves)
2818 .filter_map(|use_| self.pcurves.get(use_.pcurve.as_str()))
2819 .filter(|pcurve| {
2820 pcurve.wrapper_reversed.is_some()
2821 || pcurve.native_tail_flags.is_some()
2822 || pcurve.parameter_range.is_some()
2823 || pcurve.fit_tolerance.is_some()
2824 })
2825 .count();
2826 if reduced_pcurve_count > 0 {
2827 self.omit(
2828 LossKind::PcurveOmitted,
2829 Severity::Info,
2830 format!(
2831 "{reduced_pcurve_count} emitted coedge pcurve(s) carry native-only metadata not represented in STEP"
2832 ),
2833 );
2834 }
2835 if !self.ir.model.subds.is_empty() {
2836 self.omit(
2837 LossKind::SubdOmitted,
2838 Severity::Warning,
2839 format!(
2840 "{} subdivision surface(s) were omitted because this STEP writer \
2841 does not encode SubD control cages",
2842 self.ir.model.subds.len()
2843 ),
2844 );
2845 }
2846 let unwritten_pmi = self.ir.model.pmi.len().saturating_sub(self.written_pmi);
2847 if unwritten_pmi > 0 {
2848 self.omit(
2849 LossKind::PmiOmitted,
2850 Severity::Warning,
2851 format!("{unwritten_pmi} PMI annotation(s) were not written to STEP"),
2852 );
2853 }
2854 let source_object_count = self
2859 .ir
2860 .model
2861 .surfaces
2862 .iter()
2863 .filter(|surface| {
2864 surface
2865 .source_object
2866 .as_ref()
2867 .is_some_and(|source| source.format != "step")
2868 })
2869 .count()
2870 + self
2871 .ir
2872 .model
2873 .curves
2874 .iter()
2875 .filter(|curve| {
2876 curve
2877 .source_object
2878 .as_ref()
2879 .is_some_and(|source| source.format != "step")
2880 })
2881 .count()
2882 + self
2883 .ir
2884 .model
2885 .subds
2886 .iter()
2887 .filter(|subd| {
2888 subd.source_object
2889 .as_ref()
2890 .is_some_and(|source| source.format != "step")
2891 })
2892 .count()
2893 + self
2894 .ir
2895 .model
2896 .tessellations
2897 .iter()
2898 .filter(|tessellation| {
2899 tessellation
2900 .source_object
2901 .as_ref()
2902 .is_some_and(|source| source.format != "step")
2903 })
2904 .count();
2905 if source_object_count > 0 {
2906 self.loss(
2907 LossKind::SourceAssociationOmitted,
2908 Severity::Info,
2909 format!(
2910 "{source_object_count} source-object association(s) were not represented in STEP"
2911 ),
2912 );
2913 }
2914 let unknown_count = self
2915 .ir
2916 .native
2917 .loss_counts()
2918 .into_iter()
2919 .filter(|count| count.kind == "unknowns")
2920 .map(|count| count.count)
2921 .sum::<usize>();
2922 if unknown_count > 0 {
2923 self.loss(
2924 LossKind::PassthroughRecordOmitted,
2925 Severity::Info,
2926 format!("{unknown_count} uninterpreted passthrough record(s) were not represented in STEP"),
2927 );
2928 }
2929 if self.unstyled_colors > 0 {
2930 self.loss(
2931 LossKind::AttributesNotTransferred,
2932 Severity::Info,
2933 format!(
2934 "{} display color(s) had no emitted STEP item and were not written \
2935 to STEP presentation",
2936 self.unstyled_colors
2937 ),
2938 );
2939 }
2940 let lossy_appearances = self
2941 .ir
2942 .model
2943 .appearances
2944 .iter()
2945 .filter(|appearance| {
2946 let bindings = self
2947 .ir
2948 .model
2949 .appearance_bindings
2950 .iter()
2951 .filter(|binding| binding.appearance == appearance.id)
2952 .collect::<Vec<_>>();
2953 appearance.asset_guid.is_some()
2954 || appearance.visual_guid.is_some()
2955 || appearance.physical_token.is_some()
2956 || appearance
2957 .schema
2958 .as_deref()
2959 .is_some_and(|schema| schema != "step_surface_style")
2960 || appearance.category.is_some()
2961 || !appearance.properties.is_empty()
2962 || appearance.base_color.is_none_or(|color| color.a != 1.0)
2963 || bindings.is_empty()
2964 || bindings
2965 .iter()
2966 .any(|binding| !self.written_appearance_bindings.contains(&binding.id))
2967 })
2968 .count();
2969 if lossy_appearances > 0 {
2970 self.loss(
2971 LossKind::AppearanceReduced,
2972 Severity::Info,
2973 format!(
2974 "{lossy_appearances} appearance asset(s) were reduced to STYLED_ITEM base colors; \
2975 schemas, textures, and shader properties were not written to STEP"
2976 ),
2977 );
2978 }
2979 let lossy_binding_metadata = self
2980 .ir
2981 .model
2982 .appearance_bindings
2983 .iter()
2984 .filter(|binding| binding.object_type.is_some() || !binding.channels.is_empty())
2985 .count();
2986 if lossy_binding_metadata > 0 {
2987 self.loss(
2988 LossKind::AppearanceReduced,
2989 Severity::Info,
2990 format!(
2991 "{lossy_binding_metadata} appearance binding(s) carry source object or channel metadata not represented in STEP"
2992 ),
2993 );
2994 }
2995 if !self.ir.model.attributes.is_empty() {
2996 self.loss(
2997 LossKind::AttributesNotTransferred,
2998 Severity::Info,
2999 format!(
3000 "{} source attribute record(s) were not written to STEP",
3001 self.ir.model.attributes.len()
3002 ),
3003 );
3004 }
3005 let procedural_surface_count = self
3006 .ir
3007 .model
3008 .procedural_surfaces
3009 .iter()
3010 .filter(|procedural| !self.written_procedural_surfaces.contains(&procedural.id.0))
3011 .count();
3012 let procedural_curve_count = self
3013 .ir
3014 .model
3015 .procedural_curves
3016 .iter()
3017 .filter(|procedural| !self.written_procedural_curves.contains(&procedural.id.0))
3018 .count();
3019 if procedural_surface_count > 0 || procedural_curve_count > 0 {
3020 self.loss(
3021 LossKind::ProceduralReduced,
3022 Severity::Info,
3023 format!(
3024 "{procedural_surface_count} procedural surface definition(s) and {procedural_curve_count} procedural curve definition(s) were reduced to their solved STEP carriers"
3025 ),
3026 );
3027 }
3028 let source_native_records: usize = self
3029 .ir
3030 .native
3031 .loss_counts()
3032 .iter()
3033 .filter(|loss| loss.kind != "unknowns")
3034 .map(|loss| loss.count)
3035 .sum();
3036 if source_native_records > 0 {
3037 self.loss(
3038 LossKind::ParametricRecordOmitted,
3039 Severity::Info,
3040 format!(
3041 "{source_native_records} source-native record(s) were not represented in STEP"
3042 ),
3043 );
3044 }
3045 }
3046
3047 fn finish_report(&self) -> ExportReport {
3048 ExportReport {
3049 format: "step".into(),
3050 census: cadmpeg_ir::EntityCensus {
3051 basis: cadmpeg_ir::CensusBasis::TargetRecords,
3052 counts: self.emitter.counts(),
3053 },
3054 fidelity: FidelityResolution::NotProvided,
3055 write_path: WritePath::Synthesized,
3058 losses: self.losses.clone(),
3059 notes: self.notes.clone(),
3060 }
3061 }
3062}
3063
3064#[derive(Debug, Clone, Default)]
3066pub struct StepCodec {
3067 pub options: StepWriteOptions,
3069}
3070
3071impl Encoder for StepCodec {
3072 fn id(&self) -> &'static str {
3073 "step"
3074 }
3075
3076 fn plan<'a>(&self, input: EncodeInput<'a>) -> Result<ExportPlan<'a>, CodecError> {
3077 let mut bytes = Vec::new();
3078 let mut report =
3079 write_step(input.ir, &mut bytes, &self.options).map_err(CodecError::from)?;
3080 report.fidelity = if input.fidelity.is_some() {
3084 FidelityResolution::NotConsumed
3085 } else {
3086 FidelityResolution::NotProvided
3087 };
3088 Ok(ExportPlan::buffered(report, bytes))
3089 }
3090}
3091
3092impl Codec for StepCodec {
3093 fn id(&self) -> &'static str {
3094 "step"
3095 }
3096
3097 fn detect(&self, prefix: &[u8]) -> Confidence {
3098 if prefix.starts_with(b"ISO-10303-21;") {
3099 Confidence::High
3100 } else if is_part28_xml(prefix) {
3101 Confidence::Medium
3102 } else {
3103 Confidence::No
3104 }
3105 }
3106
3107 fn inspect_impl(
3108 &self,
3109 _ctx: &cadmpeg_core::decode::DecodeContext<'_>,
3110 root: cadmpeg_core::decode::View<'_>,
3111 ) -> Result<ContainerSummary, CodecError> {
3112 let bytes = root.window();
3113 refuse_alternate_encoding(bytes)?;
3114 if self.detect(bytes) == Confidence::No {
3115 return Err(CodecError::WrongFormat("missing ISO-10303-21 magic".into()));
3116 }
3117 let (exchange, diagnostics) =
3118 parse::parse(bytes).map_err(|error| CodecError::Malformed(error.to_string()))?;
3119 let (decoded, opaque_offsets) = reader::inspect_exchange(bytes, &exchange, &diagnostics);
3120 let mut entries = vec![ContainerEntry {
3121 name: "HEADER".into(),
3122 role: "metadata".into(),
3123 compression: "none".into(),
3124 compressed_size: 0,
3125 uncompressed_size: 0,
3126 attributes: BTreeMap::default(),
3127 }];
3128 if !exchange.anchors.is_empty() {
3129 let mut attributes = std::collections::BTreeMap::new();
3130 attributes.insert("anchor_count".into(), exchange.anchors.len().to_string());
3131 entries.push(ContainerEntry {
3132 name: "ANCHOR".into(),
3133 role: "in_file_anchors".into(),
3134 compression: "none".into(),
3135 compressed_size: 0,
3136 uncompressed_size: 0,
3137 attributes,
3138 });
3139 }
3140 if !exchange.references.is_empty() {
3141 let mut attributes = std::collections::BTreeMap::new();
3142 attributes.insert(
3143 "external_count".into(),
3144 exchange.references.len().to_string(),
3145 );
3146 attributes.insert(
3147 "external_uris".into(),
3148 exchange
3149 .references
3150 .iter()
3151 .map(|entry| entry.uri.as_str())
3152 .collect::<Vec<_>>()
3153 .join(","),
3154 );
3155 entries.push(ContainerEntry {
3156 name: "REFERENCE".into(),
3157 role: "external_references".into(),
3158 compression: "none".into(),
3159 compressed_size: 0,
3160 uncompressed_size: 0,
3161 attributes,
3162 });
3163 }
3164 for (index, section) in exchange.data.iter().enumerate() {
3165 let mut counts = std::collections::BTreeMap::<String, usize>::new();
3166 for id in §ion.records {
3167 if !opaque_offsets.contains(&exchange.records[id].span.start) {
3168 continue;
3169 }
3170 for partial in &exchange.records[id].partials {
3171 *counts.entry(partial.name.clone()).or_default() += 1;
3172 }
3173 }
3174 let unknown = counts
3175 .iter()
3176 .map(|(name, count)| format!("{name}:{count}"))
3177 .collect::<Vec<_>>()
3178 .join(",");
3179 let mut attributes = std::collections::BTreeMap::new();
3180 attributes.insert("entity_count".into(), section.records.len().to_string());
3181 attributes.insert("unknown_entities".into(), unknown);
3182 entries.push(ContainerEntry {
3183 name: format!("DATA[{index}]"),
3184 role: "entity_records".into(),
3185 compression: "none".into(),
3186 compressed_size: 0,
3187 uncompressed_size: 0,
3188 attributes,
3189 });
3190 }
3191 let external_dependencies = decoded
3192 .report
3193 .notes
3194 .iter()
3195 .filter(|note| {
3196 note.starts_with("external document ") || note.starts_with("external source ")
3197 })
3198 .cloned()
3199 .collect::<Vec<_>>();
3200 if !external_dependencies.is_empty() {
3201 let mut attributes = std::collections::BTreeMap::new();
3202 attributes.insert(
3203 "dependency_count".into(),
3204 external_dependencies.len().to_string(),
3205 );
3206 attributes.insert("dependencies".into(), external_dependencies.join(","));
3207 entries.push(ContainerEntry {
3208 name: "EXTERNAL_DEPENDENCIES".into(),
3209 role: "external_references".into(),
3210 compression: "none".into(),
3211 compressed_size: 0,
3212 uncompressed_size: 0,
3213 attributes,
3214 });
3215 }
3216 if exchange.signature.is_some() {
3217 entries.push(ContainerEntry {
3218 name: "SIGNATURE".into(),
3219 role: "signature".into(),
3220 compression: "none".into(),
3221 compressed_size: 0,
3222 uncompressed_size: 0,
3223 attributes: BTreeMap::default(),
3224 });
3225 }
3226 let schema = exchange
3227 .header
3228 .iter()
3229 .find(|record| record.name == "FILE_SCHEMA")
3230 .map_or_else(
3231 || "unspecified".into(),
3232 |record| {
3233 fn strings(value: &parse::Value, out: &mut Vec<String>) {
3234 match value {
3235 parse::Value::String(bytes) => {
3236 if let Ok(value) = strings::decode(bytes) {
3237 out.push(value);
3238 }
3239 }
3240 parse::Value::List(values) => {
3241 for value in values {
3242 strings(value, out);
3243 }
3244 }
3245 parse::Value::Typed(_, value) => strings(value, out),
3246 _ => {}
3247 }
3248 }
3249 let mut names = Vec::new();
3250 record
3251 .parameters
3252 .iter()
3253 .for_each(|value| strings(value, &mut names));
3254 names.join(",")
3255 },
3256 );
3257 let edition = if schema.contains("442 4") {
3258 "edition 3"
3259 } else if schema.contains("442 3") {
3260 "edition 2"
3261 } else if schema.contains("442 1") {
3262 "edition 1"
3263 } else {
3264 "edition unspecified"
3265 };
3266 let mut notes = vec![format!("schema {schema}; {edition}")];
3267 notes.extend(diagnostics.into_iter().map(|diagnostic| diagnostic.message));
3268 Ok(ContainerSummary {
3269 format: "step".into(),
3270 container_kind: "iso-10303-21-clear-text".into(),
3271 entries,
3272 notes,
3273 })
3274 }
3275
3276 fn decode_impl(
3277 &self,
3278 ctx: &cadmpeg_core::decode::DecodeContext<'_>,
3279 root: cadmpeg_core::decode::View<'_>,
3280 ) -> Result<DecodeResult, CodecError> {
3281 let bytes = root.window();
3282 refuse_alternate_encoding(bytes)?;
3283 if self.detect(bytes) == Confidence::No {
3284 return Err(CodecError::WrongFormat("missing ISO-10303-21 magic".into()));
3285 }
3286 reader::decode(
3287 bytes,
3288 DecodeOptions {
3289 container_only: ctx.container_only(),
3290 policy: *ctx.policy(),
3291 },
3292 )
3293 }
3294}
3295
3296fn refuse_alternate_encoding(bytes: &[u8]) -> Result<(), CodecError> {
3297 if bytes.starts_with(b"PK\x03\x04") {
3298 return Err(CodecError::NotImplemented(
3299 "STEP Part 21 ZIP container".into(),
3300 ));
3301 }
3302 if bytes.starts_with(b"\x89HDF\r\n\x1a\n") {
3303 return Err(CodecError::NotImplemented(
3304 "STEP Part 26 binary/HDF5 encoding".into(),
3305 ));
3306 }
3307 if is_part28_xml(bytes) {
3308 return Err(CodecError::NotImplemented(
3309 "STEP Part 28 XML encoding".into(),
3310 ));
3311 }
3312 let lower = bytes
3313 .iter()
3314 .take(4096)
3315 .map(u8::to_ascii_lowercase)
3316 .collect::<Vec<_>>();
3317 if lower.starts_with(b"<?xml")
3318 && (lower
3319 .windows(21)
3320 .any(|window| window == b"business_object_model")
3321 || lower.windows(14).any(|window| window == b"ap242_bo_model"))
3322 {
3323 return Err(CodecError::NotImplemented(
3324 "AP242 BO-Model XML sidecar".into(),
3325 ));
3326 }
3327 Ok(())
3328}
3329
3330fn is_part28_xml(bytes: &[u8]) -> bool {
3331 let lower = bytes
3332 .iter()
3333 .take(4096)
3334 .map(u8::to_ascii_lowercase)
3335 .collect::<Vec<_>>();
3336 lower.starts_with(b"<?xml")
3337 && (lower.windows(12).any(|window| window == b"iso_10303_28")
3338 || lower
3339 .windows(21)
3340 .any(|window| window == b"iso:std:iso:10303:-28"))
3341}
3342
3343impl From<StepError> for CodecError {
3344 fn from(error: StepError) -> Self {
3345 match error {
3346 StepError::Unsupported(message) => Self::NotImplemented(message),
3347 StepError::Io(error) => Self::Io(error),
3348 }
3349 }
3350}
3351
3352fn is_identity(rows: &[[f64; 4]; 4]) -> bool {
3353 for (i, row) in rows.iter().enumerate() {
3354 for (j, &v) in row.iter().enumerate() {
3355 let expect = if i == j { 1.0 } else { 0.0 };
3356 if (v - expect).abs() > 1e-12 {
3357 return false;
3358 }
3359 }
3360 }
3361 true
3362}
3363
3364fn is_rigid_transform(rows: &[[f64; 4]; 4]) -> bool {
3365 cadmpeg_ir::transform::Transform { rows: *rows }.is_proper_rigid()
3366}
3367
3368#[cfg(test)]
3369mod tests;