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