1use super::bodies::{collect_step_solids, step_body_claimed_shells, StepBody};
21use super::parse::{parse_data_section, Entity, Value};
22use super::*;
23use crate::feature_pipeline::pmi::annotations::fcf::characteristic_for_entity;
24use crate::feature_pipeline::pmi::{
25 PmiAnnotation, PmiCamera, PmiDisplay, PmiProjection, PmiState, PmiView,
26};
27use std::collections::BTreeMap;
28
29pub(crate) fn decode_step_text(raw: &str) -> String {
32 let mut out = String::new();
33 let mut rest = raw;
34 while let Some(index) = rest.find('\\') {
35 out.push_str(&rest[..index]);
36 let tail = &rest[index..];
37 if let Some(after) = tail.strip_prefix("\\X2\\") {
38 let end = after.find("\\X0\\").unwrap_or(after.len());
39 let hex = &after[..end];
40 let units: Vec<u16> = hex
41 .as_bytes()
42 .chunks(4)
43 .filter_map(|chunk| u16::from_str_radix(std::str::from_utf8(chunk).ok()?, 16).ok())
44 .collect();
45 out.push_str(&String::from_utf16_lossy(&units));
46 rest = &after[(end + 4).min(after.len())..];
47 } else if let Some(after) = tail.strip_prefix("\\X4\\") {
48 let end = after.find("\\X0\\").unwrap_or(after.len());
49 let hex = &after[..end];
50 for chunk in hex.as_bytes().chunks(8) {
51 if let Some(ch) = std::str::from_utf8(chunk)
52 .ok()
53 .and_then(|text| u32::from_str_radix(text, 16).ok())
54 .and_then(char::from_u32)
55 {
56 out.push(ch);
57 }
58 }
59 rest = &after[(end + 4).min(after.len())..];
60 } else if let Some(after) = tail.strip_prefix("\\X\\") {
61 if let Some(byte) = after.get(..2).and_then(|hex| u8::from_str_radix(hex, 16).ok()) {
62 out.push(byte as char);
63 rest = &after[2..];
64 } else {
65 rest = after;
66 }
67 } else if let Some(after) = tail.strip_prefix("\\N\\") {
68 out.push('\n');
69 rest = after;
70 } else if let Some(after) = tail.strip_prefix("\\S\\") {
71 let mut chars = after.chars();
72 if let Some(ch) = chars.next() {
73 out.push(((ch as u32 + 128) as u8) as char);
74 }
75 rest = chars.as_str();
76 } else if let Some(after) = tail.strip_prefix("\\\\") {
77 out.push('\\');
78 rest = after;
79 } else {
80 out.push('\\');
81 rest = &tail[1..];
82 }
83 }
84 out.push_str(rest);
85 out
86}
87
88fn arg_string(args: &[Value], index: usize) -> String {
89 match args.get(index) {
90 Some(Value::Str(text)) => decode_step_text(text),
91 _ => String::new(),
92 }
93}
94
95fn arg_ref(args: &[Value], index: usize) -> Option<usize> {
96 args.get(index).and_then(|value| value.as_ref_id().ok())
97}
98
99fn arg_refs(args: &[Value], index: usize) -> Vec<usize> {
100 args.get(index)
101 .and_then(|value| value.as_list().ok())
102 .map(|items| items.iter().filter_map(|item| item.as_ref_id().ok()).collect())
103 .unwrap_or_default()
104}
105
106fn measure_value(value: &Value) -> Option<f64> {
109 match value {
110 Value::Typed(_, inner) => inner.first().and_then(measure_value),
111 other => other.as_real().ok(),
112 }
113}
114
115pub(super) const UNRESOLVED_REF_PREFIX: &str = "STEP#";
121
122pub(super) struct NameMap {
124 faces: HashMap<usize, String>,
125 edges: HashMap<usize, String>,
126 vertices: HashMap<usize, String>,
127 pub(super) length_scale: f64,
128 angle_to_degrees: f64,
130}
131
132fn angle_scale_degrees(entities: &HashMap<usize, Entity>) -> f64 {
135 for entity in entities.values() {
136 if !entity.has("PLANE_ANGLE_UNIT") {
137 continue;
138 }
139 if let Some(args) = entity.find("CONVERSION_BASED_UNIT") {
140 if let Some(measure) = arg_ref(args, 1).and_then(|id| entities.get(&id)) {
141 if let Some(value) = measure
142 .find("PLANE_ANGLE_MEASURE_WITH_UNIT")
143 .or_else(|| measure.find("MEASURE_WITH_UNIT"))
144 .and_then(|args| args.first())
145 .and_then(measure_value)
146 {
147 return value.to_degrees();
148 }
149 }
150 }
151 }
152 1f64.to_degrees()
153}
154
155impl NameMap {
156 fn build(text: &str, entities: &HashMap<usize, Entity>, feature_name: &str) -> Result<Self, String> {
159 let imported = collect_step_solids(text)?;
160 let scale = derive_length_scale_mm(entities);
161 let resolver = Resolver::new(entities, scale);
162 let count = imported.solids.len();
163 let body_names = crate::feature_pipeline::imported_solid_names(count, feature_name);
164 let mut map = Self {
165 faces: HashMap::default(),
166 edges: HashMap::default(),
167 vertices: HashMap::default(),
168 length_scale: scale,
169 angle_to_degrees: angle_scale_degrees(entities),
170 };
171 map.add_bodies(entities, &resolver, &body_names, &imported.face_refs, "");
172 Ok(map)
173 }
174
175 pub(super) fn empty(entities: &HashMap<usize, Entity>, length_scale: f64) -> Self {
178 Self {
179 faces: HashMap::default(),
180 edges: HashMap::default(),
181 vertices: HashMap::default(),
182 length_scale,
183 angle_to_degrees: angle_scale_degrees(entities),
184 }
185 }
186
187 pub(super) fn add_bodies(
195 &mut self,
196 entities: &HashMap<usize, Entity>,
197 resolver: &Resolver,
198 body_names: &[String],
199 face_refs: &[Vec<usize>],
200 qualifier: &str,
201 ) {
202 for (body_index, body_faces) in face_refs.iter().enumerate() {
203 let Some(body_name) = body_names.get(body_index) else {
204 continue;
205 };
206 for (face_index, face_ref) in body_faces.iter().enumerate() {
208 self.faces
209 .entry(*face_ref)
210 .or_insert_with(|| format!("{qualifier}{body_name}_Face_{face_index}"));
211 }
212 let mut adjacency: HashMap<usize, Vec<String>> = HashMap::default();
217 let mut order: Vec<usize> = Vec::new();
218 for (face_index, face_ref) in body_faces.iter().enumerate() {
219 let face_name = format!("{body_name}_Face_{face_index}");
220 for edge_ref in face_edge_refs(resolver, *face_ref) {
221 let uses = adjacency.entry(edge_ref).or_default();
222 if uses.is_empty() {
223 order.push(edge_ref);
224 }
225 uses.push(face_name.clone());
226 if let Some(entity) = entities.get(&edge_ref) {
228 if let Some(args) = entity.find("EDGE_CURVE") {
229 for slot in [1usize, 2] {
230 if let Some(vertex_ref) = arg_ref(args, slot) {
231 if let Ok(point) = resolver.step_vertex_point(vertex_ref) {
232 self.vertices.entry(vertex_ref).or_insert_with(|| {
233 format!("{qualifier}{body_name}@{},{},{}", trim_real(point.x), trim_real(point.y), trim_real(point.z))
234 });
235 }
236 }
237 }
238 }
239 }
240 }
241 }
242 let mut counts: HashMap<String, usize> = HashMap::default();
243 for edge_ref in order {
244 let mut names = adjacency.remove(&edge_ref).unwrap_or_default();
245 names.sort();
246 names.dedup();
247 let base = names.join("|");
248 let n = counts.entry(base.clone()).or_default();
249 self.edges
250 .entry(edge_ref)
251 .or_insert_with(|| format!("{qualifier}{base}[{n}]"));
252 *n += 1;
253 }
254 }
255 }
256
257 fn name_of(&self, geometry_ref: usize) -> Option<String> {
258 self.faces
259 .get(&geometry_ref)
260 .or_else(|| self.edges.get(&geometry_ref))
261 .or_else(|| self.vertices.get(&geometry_ref))
262 .cloned()
263 }
264}
265
266fn trim_real(value: f64) -> String {
268 let rounded = (value * 1e9).round() / 1e9;
269 let text = format!("{rounded:.9}");
270 let trimmed = text.trim_end_matches('0').trim_end_matches('.');
271 if trimmed == "-0" || trimmed.is_empty() {
272 "0".into()
273 } else {
274 trimmed.to_string()
275 }
276}
277
278fn face_edge_refs(resolver: &Resolver, face_ref: usize) -> Vec<usize> {
280 let mut out = Vec::new();
281 let Ok(face) = resolver.get(face_ref) else {
282 return out;
283 };
284 let Some(args) = face.find("ADVANCED_FACE").or_else(|| face.find("FACE_SURFACE")) else {
285 return out;
286 };
287 for bound_ref in arg_refs(args, 1) {
288 let Ok(bound) = resolver.get(bound_ref) else { continue };
289 let Some(bound_args) = bound
290 .find("FACE_OUTER_BOUND")
291 .or_else(|| bound.find("FACE_BOUND"))
292 else {
293 continue;
294 };
295 let Some(loop_ref) = arg_ref(bound_args, 1) else { continue };
296 let Ok(edge_loop) = resolver.get(loop_ref) else { continue };
297 let Some(loop_args) = edge_loop.find("EDGE_LOOP") else { continue };
298 for oriented_ref in arg_refs(loop_args, 1) {
299 let Ok(oriented) = resolver.get(oriented_ref) else { continue };
300 let Some(oriented_args) = oriented.find("ORIENTED_EDGE") else { continue };
301 if let Some(edge_ref) = arg_ref(oriented_args, 3) {
302 out.push(edge_ref);
303 }
304 }
305 }
306 out
307}
308
309pub(super) fn body_face_refs(resolver: &Resolver, body: StepBody, built: &BrepSolid) -> Vec<usize> {
311 let Some(shells) = step_body_claimed_shells(resolver, body) else {
312 return Vec::new();
313 };
314 if shells.len() != built.shells.len() {
315 return Vec::new();
316 }
317 let mut out = Vec::new();
318 for (shell_ref, built_shell) in shells.iter().zip(&built.shells) {
319 let Some(faces) = super::bodies::shell_face_refs_pub(resolver, *shell_ref) else {
320 return Vec::new();
321 };
322 if faces.len() != built_shell.faces.len() {
323 return Vec::new();
324 }
325 out.extend(faces);
326 }
327 out
328}
329
330struct Semantic {
332 entity: usize,
333 annotation: PmiAnnotation,
334}
335
336fn aspect_names(
346 entities: &HashMap<usize, Entity>,
347 names: &NameMap,
348 pins: &HashMap<usize, usize>,
349) -> HashMap<usize, String> {
350 let mut out = HashMap::default();
351 for entity in entities.values() {
352 let Some(args) = entity.find("GEOMETRIC_ITEM_SPECIFIC_USAGE") else {
353 continue;
354 };
355 let (Some(aspect), Some(geometry)) = (arg_ref(args, 2), arg_ref(args, 4)) else {
356 continue;
357 };
358 if let Some(name) = names.name_of(geometry) {
359 out.entry(aspect).or_insert(name);
360 }
361 }
362 for entity in entities.values() {
364 let Some(args) = entity.find("ITEM_IDENTIFIED_REPRESENTATION_USAGE") else {
365 continue;
366 };
367 let Some(aspect) = arg_ref(args, 2) else { continue };
368 let geometry = match args.get(4) {
369 Some(Value::Ref(id)) => Some(*id),
370 Some(Value::Typed(_, inner)) => inner.first().and_then(|v| v.as_ref_id().ok()),
371 _ => None,
372 };
373 if let Some(name) = geometry.and_then(|g| names.name_of(g)) {
374 out.entry(aspect).or_insert(name);
375 }
376 }
377 for (aspect, occurrence) in pins {
378 if let Some(name) = out.get_mut(aspect) {
379 *name = assembly_pmi::pin_occurrence(name, *occurrence);
380 }
381 }
382 out
383}
384
385fn datum_letter(entities: &HashMap<usize, Entity>, id: usize, depth: usize) -> Option<(String, String)> {
388 if depth > 4 {
389 return None;
390 }
391 let entity = entities.get(&id)?;
392 if let Some(args) = entity.find("DATUM") {
393 return Some((arg_string(args, 4).to_ascii_uppercase(), String::new()));
394 }
395 if let Some(args) = entity.find("DATUM_REFERENCE") {
396 return datum_letter(entities, arg_ref(args, 1)?, depth + 1);
397 }
398 for keyword in ["DATUM_REFERENCE_COMPARTMENT", "DATUM_REFERENCE_ELEMENT", "GENERAL_DATUM_REFERENCE"] {
399 if let Some(args) = entity.find(keyword) {
400 let (base, modifiers) = if args.len() >= 6 {
403 (args.get(4), args.get(5))
404 } else {
405 (args.first(), args.get(1))
406 };
407 let base_ref = match base {
408 Some(Value::Ref(id)) => *id,
409 Some(Value::Typed(_, inner)) => inner.first()?.as_ref_id().ok()?,
410 _ => return None,
411 };
412 let (letter, _) = datum_letter(entities, base_ref, depth + 1)?;
413 let modifier = modifiers
414 .and_then(|value| value.as_list().ok())
415 .map(|items| items.iter().filter_map(modifier_text).next().unwrap_or_default())
416 .unwrap_or_default();
417 return Some((letter, modifier));
418 }
419 }
420 None
421}
422
423fn modifier_text(value: &Value) -> Option<String> {
424 let name = match value {
425 Value::Enum(name) => name.clone(),
426 Value::Typed(_, inner) => match inner.first()? {
427 Value::Enum(name) => name.clone(),
428 _ => return None,
429 },
430 _ => return None,
431 };
432 match name.as_str() {
433 "MAXIMUM_MATERIAL_REQUIREMENT" => Some("MMC".into()),
434 "LEAST_MATERIAL_REQUIREMENT" => Some("LMC".into()),
435 _ => None,
436 }
437}
438
439struct DimensionValues {
442 nominal: Option<f64>,
443 upper_limit: Option<f64>,
444 lower_limit: Option<f64>,
445 plus_minus: Option<(f64, f64)>,
446 callout: Option<String>,
448 angle: bool,
449}
450
451fn dimension_values(entities: &HashMap<usize, Entity>, dimension: usize, names: &NameMap) -> DimensionValues {
452 let mut values = DimensionValues {
453 nominal: None,
454 upper_limit: None,
455 lower_limit: None,
456 plus_minus: None,
457 callout: None,
458 angle: false,
459 };
460 let scale = |entity: &Entity, value: f64| -> f64 {
461 if entity.has("PLANE_ANGLE_MEASURE_WITH_UNIT") {
462 value * names.angle_to_degrees
463 } else {
464 value * names.length_scale
465 }
466 };
467 for entity in entities.values() {
468 if let Some(args) = entity.find("DIMENSIONAL_CHARACTERISTIC_REPRESENTATION") {
469 if arg_ref(args, 0) != Some(dimension) {
470 continue;
471 }
472 let Some(representation) = arg_ref(args, 1).and_then(|id| entities.get(&id)) else {
473 continue;
474 };
475 let Some(rep_args) = representation
476 .find("SHAPE_DIMENSION_REPRESENTATION")
477 .or_else(|| representation.find("REPRESENTATION"))
478 else {
479 continue;
480 };
481 for item_ref in arg_refs(rep_args, 1) {
482 let Some(item) = entities.get(&item_ref) else { continue };
483 let name = item
484 .find("REPRESENTATION_ITEM")
485 .map(|args| arg_string(args, 0))
486 .unwrap_or_default();
487 if let Some(args) = item.find("DESCRIPTIVE_REPRESENTATION_ITEM") {
488 if arg_string(args, 0) == "hole callout" {
489 values.callout = Some(arg_string(args, 1));
490 }
491 continue;
492 }
493 let Some(raw) = item
494 .find("MEASURE_WITH_UNIT")
495 .and_then(|args| args.first())
496 .and_then(measure_value)
497 else {
498 continue;
499 };
500 if item.has("PLANE_ANGLE_MEASURE_WITH_UNIT") {
501 values.angle = true;
502 }
503 let value = scale(item, raw);
504 match name.as_str() {
505 "upper limit" => values.upper_limit = Some(value),
506 "lower limit" => values.lower_limit = Some(value),
507 _ => {
508 if values.nominal.is_none() || name == "nominal value" {
509 values.nominal = Some(value);
510 }
511 }
512 }
513 }
514 }
515 if let Some(args) = entity.find("PLUS_MINUS_TOLERANCE") {
516 if arg_ref(args, 1) != Some(dimension) {
517 continue;
518 }
519 let Some(range) = arg_ref(args, 0).and_then(|id| entities.get(&id)) else {
520 continue;
521 };
522 let Some(range_args) = range.find("TOLERANCE_VALUE") else {
523 continue;
524 };
525 let bound = |slot: usize| -> Option<f64> {
526 let measure = arg_ref(range_args, slot).and_then(|id| entities.get(&id))?;
527 let raw = measure
528 .find("LENGTH_MEASURE_WITH_UNIT")
529 .or_else(|| measure.find("PLANE_ANGLE_MEASURE_WITH_UNIT"))
530 .or_else(|| measure.find("MEASURE_WITH_UNIT"))
531 .and_then(|args| args.first())
532 .and_then(measure_value)?;
533 Some(scale(measure, raw))
534 };
535 if let (Some(lower), Some(upper)) = (bound(0), bound(1)) {
536 values.plus_minus = Some((lower, upper));
537 }
538 }
539 }
540 values
541}
542
543fn apply_values(params: &mut serde_json::Map<String, serde_json::Value>, values: &DimensionValues) {
545 if let (Some(nominal), Some(upper), Some(lower)) = (values.nominal, values.upper_limit, values.lower_limit) {
546 params.insert("tolMode".into(), "limits".into());
547 params.insert("tolUpper".into(), serde_json::json!(round6(upper - nominal)));
548 params.insert("tolLower".into(), serde_json::json!(round6(nominal - lower)));
549 } else if let Some((lower, upper)) = values.plus_minus {
550 if (lower.abs() - upper.abs()).abs() < 1e-9 {
551 params.insert("tolMode".into(), "symmetric".into());
552 params.insert("tolUpper".into(), serde_json::json!(round6(upper.abs())));
553 } else {
554 params.insert("tolMode".into(), "deviation".into());
555 params.insert("tolUpper".into(), serde_json::json!(round6(upper.abs())));
556 params.insert("tolLower".into(), serde_json::json!(round6(lower.abs())));
557 }
558 }
559}
560
561fn round6(value: f64) -> f64 {
562 (value * 1e6).round() / 1e6
563}
564
565struct ViewIn {
567 name: String,
568 camera: Option<PmiCamera>,
569 callouts: Vec<usize>,
571}
572
573fn read_camera(entities: &HashMap<usize, Entity>, resolver: &Resolver, camera_ref: usize) -> Option<(String, PmiCamera)> {
574 let camera = entities.get(&camera_ref)?;
575 let args = camera.find("CAMERA_MODEL_D3")?;
576 let name = arg_string(args, 0);
577 let frame = resolver.placement(arg_ref(args, 1)?).ok()?;
578 let volume = entities.get(&arg_ref(args, 2)?)?;
579 let volume_args = volume.find("VIEW_VOLUME")?;
580 let parallel = volume_args.first().map(|v| v.enum_is("PARALLEL")).unwrap_or(true);
581 let distance = resolver.length(volume_args.get(2)?.as_real().ok()?).max(1e-6);
582 let window = entities.get(&arg_ref(volume_args, 8)?)?;
583 let window_args = window.find("PLANAR_BOX")?;
584 let width = resolver.length(window_args.get(1)?.as_real().ok()?);
585 let height = resolver.length(window_args.get(2)?.as_real().ok()?).max(1e-9);
586 let eye = frame.origin;
587 let target = eye.add(frame.z.scale(distance));
588 let projection = if parallel {
589 PmiProjection::Orthographic {
590 half_height: height * 0.5,
591 }
592 } else {
593 PmiProjection::Perspective {
594 fov_y_deg: 2.0 * (height * 0.5 / distance).atan().to_degrees(),
595 }
596 };
597 let aspect = if height > 0.0 { width / height } else { 1.5 };
598 Some((
599 name,
600 PmiCamera {
601 eye: [eye.x, eye.y, eye.z],
602 target: [target.x, target.y, target.z],
603 up: [frame.y.x, frame.y.y, frame.y.z],
604 projection,
605 viewport: [800.0 * aspect, 800.0],
606 },
607 ))
608}
609
610fn callout_contents(entity: &Entity) -> Option<&[Value]> {
623 entity
624 .records
625 .iter()
626 .find(|(keyword, _)| keyword.ends_with("CALLOUT"))
627 .map(|(_, args)| args.as_slice())
628}
629
630fn is_callout(entity: &Entity) -> bool {
632 callout_contents(entity).is_some()
633}
634
635fn text_literal(entity: &Entity) -> Option<String> {
641 entity
642 .records
643 .iter()
644 .find(|(keyword, _)| keyword.starts_with("TEXT_LITERAL"))
645 .map(|(_, args)| arg_string(args, 1))
646 .filter(|text| !text.is_empty())
647}
648
649fn presentation_children(entity: &Entity) -> Vec<usize> {
654 let mut out = Vec::new();
655 let mut take = |args: &[Value], slot: usize| match args.get(slot) {
656 Some(Value::Ref(target)) => out.push(*target),
657 Some(Value::List(items)) => {
658 out.extend(items.iter().filter_map(|item| item.as_ref_id().ok()))
659 }
660 _ => {}
661 };
662 for (keyword, args) in &entity.records {
663 let slot = if keyword.ends_with("CALLOUT")
664 || keyword.starts_with("COMPOSITE_TEXT")
665 || keyword == "GEOMETRIC_CURVE_SET"
666 || keyword == "STYLED_ITEM"
667 {
668 1
669 } else if keyword.ends_with("ANNOTATION_OCCURRENCE")
670 || keyword == "ANNOTATION_CURVE_OCCURRENCE"
671 || keyword == "ANNOTATION_TEXT_OCCURRENCE"
672 || keyword == "ANNOTATION_SYMBOL_OCCURRENCE"
673 {
674 2
675 } else {
676 continue;
677 };
678 take(args, slot);
679 }
680 out
681}
682
683fn expand_item(entities: &HashMap<usize, Entity>, item: usize, out: &mut Vec<usize>) {
685 let Some(entity) = entities.get(&item) else { return };
686 if let Some(args) = entity.find("ANNOTATION_PLANE") {
687 for element in arg_refs(args, 3) {
688 expand_item(entities, element, out);
689 }
690 } else if is_callout(entity) || entity.has("ANNOTATION_OCCURRENCE") || entity.has("ANNOTATION_CURVE_OCCURRENCE") || entity.has("ANNOTATION_TEXT_OCCURRENCE") {
691 out.push(item);
692 }
693}
694
695fn plane_of_callouts(entities: &HashMap<usize, Entity>) -> HashMap<usize, usize> {
698 let mut planes: Vec<(&usize, &Entity)> = entities.iter().filter(|(_, e)| e.has("ANNOTATION_PLANE")).collect();
699 planes.sort_by_key(|(id, _)| **id);
700 let mut out: HashMap<usize, usize> = HashMap::default();
701 for (id, entity) in planes {
702 let Some(args) = entity.find("ANNOTATION_PLANE") else { continue };
703 let mut members = Vec::new();
704 for element in arg_refs(args, 3) {
705 expand_item(entities, element, &mut members);
706 }
707 for member in members {
708 out.entry(member).or_insert(*id);
709 }
710 }
711 out
712}
713
714fn plane_face_name(
719 entities: &HashMap<usize, Entity>,
720 resolver: &Resolver,
721 names: &NameMap,
722 plane: usize,
723) -> Option<String> {
724 let args = entities.get(&plane)?.find("ANNOTATION_PLANE")?;
725 let item = arg_ref(args, 2)?;
726 let placement = match entities.get(&item) {
727 Some(entity) if entity.has("PLANE") => arg_ref(entity.find("PLANE")?, 1)?,
728 Some(entity) if entity.has("AXIS2_PLACEMENT_3D") => item,
729 _ => return None,
730 };
731 let frame = resolver.placement(placement).ok()?;
732 let normal = frame.z.normalized().ok()?;
733 let tolerance = 1e-4 * frame.origin.length().max(1.0);
734 let mut faces: Vec<(&usize, &String)> = names.faces.iter().collect();
735 faces.sort();
736 for (face_id, name) in faces {
737 let Some(face_args) = entities.get(face_id).and_then(|e| e.find("ADVANCED_FACE")) else { continue };
738 let Some(surface) = arg_ref(face_args, 2).and_then(|id| entities.get(&id)) else { continue };
739 let Some(plane_args) = surface.find("PLANE") else { continue };
740 let Some(face_frame) = arg_ref(plane_args, 1).and_then(|p| resolver.placement(p).ok()) else { continue };
741 let Ok(face_normal) = face_frame.z.normalized() else { continue };
742 if face_normal.dot(normal).abs() < 1.0 - 1e-6 {
743 continue;
744 }
745 if face_frame.origin.sub(frame.origin).dot(normal).abs() > tolerance {
746 continue;
747 }
748 return Some(name.clone());
749 }
750 None
751}
752
753#[derive(Default)]
757struct ValidationProps {
758 unicode: HashMap<usize, String>,
759 centre: HashMap<usize, [f64; 3]>,
760}
761
762impl ValidationProps {
763 fn read(entities: &HashMap<usize, Entity>, resolver: &Resolver) -> Self {
764 let mut out = Self::default();
765 for entity in entities.values() {
766 let Some(args) = entity.find("PROPERTY_DEFINITION_REPRESENTATION") else { continue };
767 let (Some(definition), Some(representation)) = (arg_ref(args, 0), arg_ref(args, 1)) else { continue };
768 let Some(within) = entities
769 .get(&definition)
770 .and_then(|e| e.find("PROPERTY_DEFINITION"))
771 .and_then(|a| arg_ref(a, 2))
772 else {
773 continue;
774 };
775 let Some(item) = entities
776 .get(&within)
777 .and_then(|e| e.find("CHARACTERIZED_ITEM_WITHIN_REPRESENTATION"))
778 .and_then(|a| arg_ref(a, 2))
779 else {
780 continue;
781 };
782 let Some(items) = entities.get(&representation).and_then(|e| e.find("REPRESENTATION")) else { continue };
783 for value in arg_refs(items, 1) {
784 let Some(value_entity) = entities.get(&value) else { continue };
785 if let Some(dri) = value_entity.find("DESCRIPTIVE_REPRESENTATION_ITEM") {
786 if arg_string(dri, 0) == "equivalent unicode string" {
787 out.unicode.entry(item).or_insert_with(|| arg_string(dri, 1));
788 }
789 }
790 if let Some(point) = value_entity.find("CARTESIAN_POINT") {
791 if arg_string(point, 0) == "polyline centre point" {
792 if let Ok(p) = resolver.point(value) {
793 out.centre.entry(item).or_insert([p.x, p.y, p.z]);
794 }
795 }
796 }
797 }
798 }
799 out
800 }
801}
802
803fn callout_label(
809 entities: &HashMap<usize, Entity>,
810 resolver: &Resolver,
811 props: &ValidationProps,
812 callout: usize,
813) -> (Option<[f64; 3]>, String) {
814 let unicode = props.unicode.get(&callout).cloned().unwrap_or_default();
815 let mut position = entities
816 .get(&callout)
817 .and_then(|entity| callout_contents(entity))
818 .map(|args| arg_refs(args, 1))
819 .unwrap_or_default()
820 .iter()
821 .find_map(|subset| props.centre.get(subset).copied())
822 .or_else(|| props.centre.get(&callout).copied());
823 let mut fallback = None;
824 let mut literals: Vec<String> = Vec::new();
829 let mut visit = |id: usize, entity: &Entity| {
830 if let Some(literal) = text_literal(entity) {
831 literals.push(literal);
832 if position.is_none() {
833 if let Some(frame) = entity
834 .records
835 .iter()
836 .find(|(keyword, _)| keyword.starts_with("TEXT_LITERAL"))
837 .and_then(|(_, args)| arg_ref(args, 2))
838 .and_then(|placement| resolver.placement(placement).ok())
839 {
840 position = Some([frame.origin.x, frame.origin.y, frame.origin.z]);
841 }
842 }
843 }
844 if let Some(args) = entity.find("POLYLINE") {
845 if fallback.is_none() {
846 if let Some(point) = arg_refs(args, 1).first().and_then(|p| resolver.point(*p).ok()) {
847 fallback = Some([point.x, point.y, point.z]);
848 }
849 }
850 }
851 let _ = id;
852 };
853 let mut stack = vec![callout];
856 let mut seen: HashSet<usize> = HashSet::default();
857 while let Some(id) = stack.pop() {
858 if !seen.insert(id) {
859 continue;
860 }
861 let Some(entity) = entities.get(&id) else { continue };
862 visit(id, entity);
863 let mut children = presentation_children(entity);
864 children.reverse();
865 stack.extend(children);
866 }
867 let text = if unicode.is_empty() {
868 literals.join("\n")
869 } else {
870 unicode
871 };
872 (position.or(fallback), text)
873}
874
875pub fn read_step_pmi(text: &str, feature_name: &str) -> Result<Option<PmiState>, String> {
879 if !file_mentions_pmi(text) {
880 return Ok(None);
881 }
882 let entities = parse_data_section(text)?;
883 let names = NameMap::build(text, &entities, feature_name)?;
884 Ok(lift_pmi(text, &entities, &names, &HashMap::default(), &mut Vec::new()))
885}
886
887pub(super) fn file_mentions_pmi(text: &str) -> bool {
890 text.contains("DIMENSIONAL_")
891 || text.contains("_TOLERANCE(")
892 || text.contains("DATUM(")
893 || text.contains("CAMERA_MODEL_D3(")
894 || text.contains("CALLOUT(")
895}
896
897pub(super) fn lift_pmi(
907 text: &str,
908 entities: &HashMap<usize, Entity>,
909 names: &NameMap,
910 pins: &HashMap<usize, usize>,
911 diagnostics: &mut Vec<String>,
912) -> Option<PmiState> {
913 let resolver = Resolver::new(entities, names.length_scale);
914 let aspects = aspect_names(entities, names, pins);
915 let mut unresolved: Vec<usize> = Vec::new();
919 let mut aspect_ref = |id: usize| -> String {
920 match aspects.get(&id) {
921 Some(name) => name.clone(),
922 None => {
923 unresolved.push(id);
924 format!("{UNRESOLVED_REF_PREFIX}{id}")
925 }
926 }
927 };
928 let mut ids: BTreeMap<usize, &Entity> = entities.iter().map(|(id, entity)| (*id, entity)).collect();
929 let mut state = PmiState::default();
930 let mut semantics: Vec<Semantic> = Vec::new();
931 let mut params = |pairs: Vec<(&str, serde_json::Value)>| -> serde_json::Map<String, serde_json::Value> {
932 let mut map = serde_json::Map::new();
933 for (key, value) in pairs {
934 map.insert(key.to_string(), value);
935 }
936 map
937 };
938
939 let mut datum_ids: HashMap<usize, String> = HashMap::default();
941 for (id, entity) in ids.iter() {
942 let Some(args) = entity.find("DATUM") else { continue };
943 let letter = arg_string(args, 4).to_ascii_uppercase();
944 if letter.is_empty() {
945 continue;
946 }
947 let feature = entities.values().find_map(|candidate| {
949 let rel = candidate.find("SHAPE_ASPECT_RELATIONSHIP")?;
950 (arg_ref(rel, 3) == Some(*id)).then(|| arg_ref(rel, 2)).flatten()
951 });
952 let Some(target) = feature.map(&mut aspect_ref) else {
953 continue; };
955 let annotation_id = state.next_id("DTM");
956 datum_ids.insert(*id, letter.clone());
957 semantics.push(Semantic {
958 entity: *id,
959 annotation: PmiAnnotation {
960 plugin_replay: None,
961 persistent_data: serde_json::Value::Null,
962 kind: "datum".into(),
963 enabled: true,
964 params: serde_json::Value::Object(params(vec![
965 ("id", annotation_id.into()),
966 ("target", target.into()),
967 ("letter", letter.into()),
968 ])),
969 label_world: None,
970 },
971 });
972 }
973
974 for (id, entity) in ids.iter() {
976 let mut fields: Option<(&str, serde_json::Map<String, serde_json::Value>)> = None;
977 if let Some(args) = entity.find("ANGULAR_LOCATION").or_else(|| entity.find("DIMENSIONAL_LOCATION")) {
978 let (Some(a), Some(b)) = (
979 arg_ref(args, 2).map(&mut aspect_ref),
980 arg_ref(args, 3).map(&mut aspect_ref),
981 ) else {
982 continue; };
984 let values = dimension_values(entities, *id, &names);
985 if entity.has("ANGULAR_LOCATION") {
986 let mut map = params(vec![("targets", serde_json::json!([a, b])), ("decimals", 1.into())]);
987 if let Some(nominal) = values.nominal {
988 let kind = if nominal > 180.0 { "reflex" } else if nominal > 90.0 + 1e-9 { "obtuse" } else { "acute" };
989 map.insert("angleType".into(), kind.into());
990 }
991 apply_values(&mut map, &values);
992 fields = Some(("angle", map));
993 } else {
994 let mut map = params(vec![("targets", serde_json::json!([a, b]))]);
995 if let Some(orientation) = orientation_axis(entities, *id) {
996 map.insert("alignment".into(), orientation.into());
997 }
998 apply_values(&mut map, &values);
999 fields = Some(("linear", map));
1000 }
1001 } else if let Some(args) = entity.find("DIMENSIONAL_SIZE") {
1002 let Some(target) = arg_ref(args, 0).map(&mut aspect_ref) else {
1003 continue; };
1005 let name = arg_string(args, 1).to_ascii_lowercase();
1006 let values = dimension_values(entities, *id, &names);
1007 if let Some(callout) = &values.callout {
1008 let _ = callout;
1009 let map = params(vec![("target", target.into()), ("showQuantity", true.into())]);
1010 fields = Some(("holeCallout", map));
1011 } else if name.contains("diameter") || name.contains("radius") {
1012 let mut map = params(vec![
1013 ("target", target.into()),
1014 ("displayStyle", if name.contains("radius") { "radius" } else { "diameter" }.into()),
1015 ]);
1016 apply_values(&mut map, &values);
1017 fields = Some(("radial", map));
1018 } else {
1019 let mut map = params(vec![("targets", serde_json::json!([target]))]);
1020 apply_values(&mut map, &values);
1021 fields = Some(("linear", map));
1022 }
1023 }
1024 if let Some((kind, mut map)) = fields {
1025 let prefix = match kind {
1026 "angle" => "ANG",
1027 "radial" => "RAD",
1028 "holeCallout" => "HOLE",
1029 _ => "DIM",
1030 };
1031 map.insert("id".into(), state.next_id(prefix).into());
1032 semantics.push(Semantic {
1033 entity: *id,
1034 annotation: PmiAnnotation {
1035 plugin_replay: None,
1036 persistent_data: serde_json::Value::Null,
1037 kind: kind.into(),
1038 enabled: true,
1039 params: serde_json::Value::Object(map),
1040 label_world: None,
1041 },
1042 });
1043 }
1044 }
1045
1046 for (id, entity) in ids.iter() {
1048 let Some((kind_record, base_args)) = entity.records.iter().find_map(|(keyword, args)| {
1049 characteristic_for_entity(keyword).map(|c| (c, args))
1050 }) else {
1051 continue;
1052 };
1053 let args = entity.find("GEOMETRIC_TOLERANCE").unwrap_or(base_args);
1056 if args.len() < 4 {
1057 continue;
1058 }
1059 let Some(target) = arg_ref(args, 3).map(&mut aspect_ref) else {
1060 continue; };
1062 let magnitude = arg_ref(args, 2)
1063 .and_then(|m| entities.get(&m))
1064 .and_then(|m| m.find("LENGTH_MEASURE_WITH_UNIT").or_else(|| m.find("MEASURE_WITH_UNIT")))
1065 .and_then(|a| a.first())
1066 .and_then(measure_value)
1067 .map(|v| v * names.length_scale)
1068 .unwrap_or(0.1);
1069 let mut map = params(vec![
1070 ("target", target.into()),
1071 ("characteristic", kind_record.id.into()),
1072 ("zoneValue", serde_json::json!(round6(magnitude))),
1073 ]);
1074 if let Some(mod_args) = entity.find("GEOMETRIC_TOLERANCE_WITH_MODIFIERS") {
1075 if let Some(items) = mod_args.first().and_then(|v| v.as_list().ok()) {
1076 if let Some(modifier) = items.iter().filter_map(modifier_text).next() {
1077 map.insert("materialCondition".into(), modifier.into());
1078 }
1079 }
1080 }
1081 if let Some(ref_args) = entity.find("GEOMETRIC_TOLERANCE_WITH_DATUM_REFERENCE") {
1082 let mut letters: Vec<(String, String)> = Vec::new();
1083 for system_ref in arg_refs(ref_args, 0) {
1084 let Some(system) = entities.get(&system_ref) else { continue };
1085 if let Some(system_args) = system.find("DATUM_SYSTEM") {
1086 for constituent in arg_refs(system_args, 4) {
1087 if let Some(pair) = datum_letter(entities, constituent, 0) {
1088 letters.push(pair);
1089 }
1090 }
1091 } else if let Some(pair) = datum_letter(entities, system_ref, 0) {
1092 letters.push(pair);
1093 }
1094 }
1095 for (slot, (letter, modifier)) in ["A", "B", "C"].iter().zip(letters) {
1096 map.insert(format!("datum{slot}"), letter.into());
1097 if !modifier.is_empty() {
1098 map.insert(format!("datum{slot}Modifier"), modifier.into());
1099 }
1100 }
1101 }
1102 let cylindrical = entities.values().any(|zone| {
1103 zone.find("TOLERANCE_ZONE")
1104 .map(|zone_args| {
1105 arg_refs(zone_args, 4).contains(id)
1106 && arg_ref(zone_args, 5)
1107 .and_then(|form| entities.get(&form))
1108 .and_then(|form| form.find("TOLERANCE_ZONE_FORM"))
1109 .map(|form_args| arg_string(form_args, 0).to_ascii_lowercase().contains("cylindrical"))
1110 .unwrap_or(false)
1111 })
1112 .unwrap_or(false)
1113 });
1114 if cylindrical {
1115 map.insert("zoneDiameter".into(), true.into());
1116 }
1117 map.insert("id".into(), state.next_id("FCF").into());
1118 semantics.push(Semantic {
1119 entity: *id,
1120 annotation: PmiAnnotation {
1121 plugin_replay: None,
1122 persistent_data: serde_json::Value::Null,
1123 kind: "fcf".into(),
1124 enabled: true,
1125 params: serde_json::Value::Object(map),
1126 label_world: None,
1127 },
1128 });
1129 }
1130 ids.clear();
1131
1132 let mut callout_of: HashMap<usize, usize> = HashMap::default();
1134 for entity in entities.values() {
1135 let Some(args) = entity.find("DRAUGHTING_MODEL_ITEM_ASSOCIATION") else { continue };
1136 if let (Some(definition), Some(callout)) = (arg_ref(args, 2), arg_ref(args, 4)) {
1137 callout_of.entry(definition).or_insert(callout);
1138 }
1139 }
1140 let datum_feature_of: HashMap<usize, usize> = entities
1142 .values()
1143 .filter_map(|entity| {
1144 let rel = entity.find("SHAPE_ASPECT_RELATIONSHIP")?;
1145 Some((arg_ref(rel, 3)?, arg_ref(rel, 2)?))
1146 })
1147 .collect();
1148 let mut linked_callouts: HashSet<usize> = HashSet::default();
1149 let mut annotations: Vec<(Option<usize>, PmiAnnotation)> = Vec::new();
1150 let plane_of = plane_of_callouts(entities);
1154 let props = ValidationProps::read(entities, &resolver);
1155 let mut plane_faces: HashMap<usize, Option<String>> = HashMap::default();
1156 let mut plane_param = |callout: usize, annotation: &mut PmiAnnotation| {
1157 let Some(plane) = plane_of.get(&callout).copied() else { return };
1158 let face = plane_faces
1159 .entry(plane)
1160 .or_insert_with(|| plane_face_name(entities, &resolver, &names, plane))
1161 .clone();
1162 if let (Some(face), Some(object)) = (face, annotation.params.as_object_mut()) {
1163 object.insert("plane".into(), serde_json::Value::String(face));
1164 }
1165 };
1166 for semantic in semantics {
1167 let callout = callout_of
1168 .get(&semantic.entity)
1169 .copied()
1170 .or_else(|| datum_feature_of.get(&semantic.entity).and_then(|f| callout_of.get(f)).copied());
1171 let mut annotation = semantic.annotation;
1172 if let Some(callout) = callout {
1173 linked_callouts.insert(callout);
1174 let (position, _) = callout_label(entities, &resolver, &props, callout);
1175 annotation.label_world = position;
1176 plane_param(callout, &mut annotation);
1177 }
1178 annotations.push((callout, annotation));
1179 }
1180 for (id, entity) in entities.iter() {
1182 if !is_callout(entity) || linked_callouts.contains(id) {
1183 continue;
1184 }
1185 let (position, text) = callout_label(entities, &resolver, &props, *id);
1186 if text.trim().is_empty() {
1187 continue;
1188 }
1189 let annotation_id = state.next_id("NOTE");
1190 let mut annotation = PmiAnnotation {
1191 plugin_replay: None,
1192 persistent_data: serde_json::Value::Null,
1193 kind: "note".into(),
1194 enabled: true,
1195 params: serde_json::Value::Object(params(vec![("id", annotation_id.into()), ("text", text.into())])),
1196 label_world: position,
1197 };
1198 plane_param(*id, &mut annotation);
1199 annotations.push((Some(*id), annotation));
1200 }
1201 if annotations.is_empty() && !text.contains("CAMERA_MODEL_D3(") {
1202 return None;
1203 }
1204
1205 let mut views_in: Vec<ViewIn> = Vec::new();
1207 let mut models: Vec<(&usize, &Entity)> = entities.iter().filter(|(_, e)| e.has("DRAUGHTING_MODEL")).collect();
1208 models.sort_by_key(|(id, _)| **id);
1209 for (_, model) in models {
1210 let Some(args) = model.find("DRAUGHTING_MODEL") else { continue };
1211 let items = arg_refs(args, 1);
1212 let camera = items
1213 .iter()
1214 .find_map(|item| read_camera(entities, &resolver, *item));
1215 let Some((camera_name, camera)) = camera else {
1216 continue; };
1218 let mut callouts = Vec::new();
1219 for item in &items {
1220 expand_item(entities, *item, &mut callouts);
1221 }
1222 let model_name = arg_string(args, 0);
1223 views_in.push(ViewIn {
1224 name: if camera_name.trim().is_empty() { model_name } else { camera_name },
1225 camera: Some(camera),
1226 callouts,
1227 });
1228 }
1229 let mut placed: HashSet<usize> = HashSet::default();
1230 for view_in in views_in {
1231 let id = state.next_id("VIEW");
1232 let mut view = PmiView {
1233 id,
1234 name: view_in.name,
1235 camera: view_in.camera,
1236 display: PmiDisplay::default(),
1237 annotations: Vec::new(),
1238 };
1239 for (index, (callout, annotation)) in annotations.iter().enumerate() {
1240 if let Some(callout) = callout {
1241 if view_in.callouts.contains(callout) && !placed.contains(&index) {
1242 view.annotations.push(annotation.clone());
1243 placed.insert(index);
1244 }
1245 }
1246 }
1247 state.views.push(view);
1248 }
1249 let unplaced: Vec<PmiAnnotation> = annotations
1250 .iter()
1251 .enumerate()
1252 .filter(|(index, _)| !placed.contains(index))
1253 .map(|(_, (_, annotation))| annotation.clone())
1254 .collect();
1255 if !unplaced.is_empty() {
1256 let id = state.next_id("VIEW");
1257 state.views.push(PmiView {
1258 id,
1259 name: "Imported PMI".into(),
1260 camera: None,
1261 display: PmiDisplay::default(),
1262 annotations: unplaced,
1263 });
1264 }
1265 if state.views.is_empty() {
1266 return None;
1267 }
1268 for aspect in unresolved {
1269 diagnostics.push(format!(
1270 "PMI: shape aspect #{aspect} names geometry this import did not build; \
1271 the annotation is kept with an unresolved reference"
1272 ));
1273 }
1274 Some(state)
1275}
1276
1277fn orientation_axis(entities: &HashMap<usize, Entity>, dimension: usize) -> Option<&'static str> {
1279 for entity in entities.values() {
1280 let Some(args) = entity.find("DIMENSIONAL_CHARACTERISTIC_REPRESENTATION") else { continue };
1281 if arg_ref(args, 0) != Some(dimension) {
1282 continue;
1283 }
1284 let Some(representation) = arg_ref(args, 1).and_then(|id| entities.get(&id)) else { continue };
1285 let Some(rep_args) = representation.find("SHAPE_DIMENSION_REPRESENTATION") else { continue };
1286 for item_ref in arg_refs(rep_args, 1) {
1287 let Some(item) = entities.get(&item_ref) else { continue };
1288 let Some(placement) = item.find("AXIS2_PLACEMENT_3D") else { continue };
1289 if arg_string(placement, 0) != "orientation" {
1290 continue;
1291 }
1292 let Some(direction) = arg_ref(placement, 3).and_then(|id| entities.get(&id)) else { continue };
1293 let Some(list) = direction.find("DIRECTION").and_then(|d| d.get(1)).and_then(|v| v.as_list().ok()) else { continue };
1294 let components: Vec<f64> = list.iter().filter_map(|v| v.as_real().ok()).collect();
1295 let axis = components
1296 .iter()
1297 .enumerate()
1298 .max_by(|a, b| a.1.abs().partial_cmp(&b.1.abs()).unwrap_or(std::cmp::Ordering::Equal))
1299 .map(|(index, _)| index)?;
1300 return Some(["X", "Y", "Z"][axis.min(2)]);
1301 }
1302 }
1303 None
1304}