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
115struct NameMap {
117 faces: HashMap<usize, String>,
118 edges: HashMap<usize, String>,
119 vertices: HashMap<usize, String>,
120 length_scale: f64,
121 angle_to_degrees: f64,
123}
124
125fn angle_scale_degrees(entities: &HashMap<usize, Entity>) -> f64 {
128 for entity in entities.values() {
129 if !entity.has("PLANE_ANGLE_UNIT") {
130 continue;
131 }
132 if let Some(args) = entity.find("CONVERSION_BASED_UNIT") {
133 if let Some(measure) = arg_ref(args, 1).and_then(|id| entities.get(&id)) {
134 if let Some(value) = measure
135 .find("PLANE_ANGLE_MEASURE_WITH_UNIT")
136 .or_else(|| measure.find("MEASURE_WITH_UNIT"))
137 .and_then(|args| args.first())
138 .and_then(measure_value)
139 {
140 return value.to_degrees();
141 }
142 }
143 }
144 }
145 1f64.to_degrees()
146}
147
148impl NameMap {
149 fn build(text: &str, entities: &HashMap<usize, Entity>, feature_name: &str) -> Result<Self, String> {
150 let imported = collect_step_solids(text)?;
151 let resolver = Resolver {
152 entities,
153 length_scale: derive_length_scale_mm(entities),
154 };
155 let count = imported.solids.len();
156 let body_names = crate::feature_pipeline::imported_solid_names(count, feature_name);
157 let mut faces = HashMap::default();
158 let mut edges = HashMap::default();
159 let mut vertices = HashMap::default();
160 for (body_index, body_faces) in imported.face_refs.iter().enumerate() {
161 let Some(body_name) = body_names.get(body_index) else {
162 continue;
163 };
164 for (face_index, face_ref) in body_faces.iter().enumerate() {
166 faces
167 .entry(*face_ref)
168 .or_insert_with(|| format!("{body_name}_Face_{face_index}"));
169 }
170 let mut adjacency: HashMap<usize, Vec<String>> = HashMap::default();
174 let mut order: Vec<usize> = Vec::new();
175 for (face_index, face_ref) in body_faces.iter().enumerate() {
176 let face_name = format!("{body_name}_Face_{face_index}");
177 for edge_ref in face_edge_refs(&resolver, *face_ref) {
178 let uses = adjacency.entry(edge_ref).or_default();
179 if uses.is_empty() {
180 order.push(edge_ref);
181 }
182 uses.push(face_name.clone());
183 if let Some(entity) = entities.get(&edge_ref) {
185 if let Some(args) = entity.find("EDGE_CURVE") {
186 for slot in [1usize, 2] {
187 if let Some(vertex_ref) = arg_ref(args, slot) {
188 if let Ok(point) = resolver.step_vertex_point(vertex_ref) {
189 vertices.entry(vertex_ref).or_insert_with(|| {
190 format!("{body_name}@{},{},{}", trim_real(point.x), trim_real(point.y), trim_real(point.z))
191 });
192 }
193 }
194 }
195 }
196 }
197 }
198 }
199 let mut counts: HashMap<String, usize> = HashMap::default();
200 for edge_ref in order {
201 let mut names = adjacency.remove(&edge_ref).unwrap_or_default();
202 names.sort();
203 names.dedup();
204 let base = names.join("|");
205 let n = counts.entry(base.clone()).or_default();
206 edges.entry(edge_ref).or_insert_with(|| format!("{base}[{n}]"));
207 *n += 1;
208 }
209 }
210 Ok(Self {
211 faces,
212 edges,
213 vertices,
214 length_scale: resolver.length_scale,
215 angle_to_degrees: angle_scale_degrees(entities),
216 })
217 }
218
219 fn name_of(&self, geometry_ref: usize) -> Option<String> {
220 self.faces
221 .get(&geometry_ref)
222 .or_else(|| self.edges.get(&geometry_ref))
223 .or_else(|| self.vertices.get(&geometry_ref))
224 .cloned()
225 }
226}
227
228fn trim_real(value: f64) -> String {
230 let rounded = (value * 1e9).round() / 1e9;
231 let text = format!("{rounded:.9}");
232 let trimmed = text.trim_end_matches('0').trim_end_matches('.');
233 if trimmed == "-0" || trimmed.is_empty() {
234 "0".into()
235 } else {
236 trimmed.to_string()
237 }
238}
239
240fn face_edge_refs(resolver: &Resolver, face_ref: usize) -> Vec<usize> {
242 let mut out = Vec::new();
243 let Ok(face) = resolver.get(face_ref) else {
244 return out;
245 };
246 let Some(args) = face.find("ADVANCED_FACE").or_else(|| face.find("FACE_SURFACE")) else {
247 return out;
248 };
249 for bound_ref in arg_refs(args, 1) {
250 let Ok(bound) = resolver.get(bound_ref) else { continue };
251 let Some(bound_args) = bound
252 .find("FACE_OUTER_BOUND")
253 .or_else(|| bound.find("FACE_BOUND"))
254 else {
255 continue;
256 };
257 let Some(loop_ref) = arg_ref(bound_args, 1) else { continue };
258 let Ok(edge_loop) = resolver.get(loop_ref) else { continue };
259 let Some(loop_args) = edge_loop.find("EDGE_LOOP") else { continue };
260 for oriented_ref in arg_refs(loop_args, 1) {
261 let Ok(oriented) = resolver.get(oriented_ref) else { continue };
262 let Some(oriented_args) = oriented.find("ORIENTED_EDGE") else { continue };
263 if let Some(edge_ref) = arg_ref(oriented_args, 3) {
264 out.push(edge_ref);
265 }
266 }
267 }
268 out
269}
270
271pub(super) fn body_face_refs(resolver: &Resolver, body: StepBody, built: &BrepSolid) -> Vec<usize> {
273 let Some(shells) = step_body_claimed_shells(resolver, body) else {
274 return Vec::new();
275 };
276 if shells.len() != built.shells.len() {
277 return Vec::new();
278 }
279 let mut out = Vec::new();
280 for (shell_ref, built_shell) in shells.iter().zip(&built.shells) {
281 let Some(faces) = super::bodies::shell_face_refs_pub(resolver, *shell_ref) else {
282 return Vec::new();
283 };
284 if faces.len() != built_shell.faces.len() {
285 return Vec::new();
286 }
287 out.extend(faces);
288 }
289 out
290}
291
292struct Semantic {
294 entity: usize,
295 annotation: PmiAnnotation,
296}
297
298fn aspect_names(entities: &HashMap<usize, Entity>, names: &NameMap) -> HashMap<usize, String> {
301 let mut out = HashMap::default();
302 for entity in entities.values() {
303 let Some(args) = entity.find("GEOMETRIC_ITEM_SPECIFIC_USAGE") else {
304 continue;
305 };
306 let (Some(aspect), Some(geometry)) = (arg_ref(args, 2), arg_ref(args, 4)) else {
307 continue;
308 };
309 if let Some(name) = names.name_of(geometry) {
310 out.entry(aspect).or_insert(name);
311 }
312 }
313 for entity in entities.values() {
315 let Some(args) = entity.find("ITEM_IDENTIFIED_REPRESENTATION_USAGE") else {
316 continue;
317 };
318 let Some(aspect) = arg_ref(args, 2) else { continue };
319 let geometry = match args.get(4) {
320 Some(Value::Ref(id)) => Some(*id),
321 Some(Value::Typed(_, inner)) => inner.first().and_then(|v| v.as_ref_id().ok()),
322 _ => None,
323 };
324 if let Some(name) = geometry.and_then(|g| names.name_of(g)) {
325 out.entry(aspect).or_insert(name);
326 }
327 }
328 out
329}
330
331fn datum_letter(entities: &HashMap<usize, Entity>, id: usize, depth: usize) -> Option<(String, String)> {
334 if depth > 4 {
335 return None;
336 }
337 let entity = entities.get(&id)?;
338 if let Some(args) = entity.find("DATUM") {
339 return Some((arg_string(args, 4).to_ascii_uppercase(), String::new()));
340 }
341 if let Some(args) = entity.find("DATUM_REFERENCE") {
342 return datum_letter(entities, arg_ref(args, 1)?, depth + 1);
343 }
344 for keyword in ["DATUM_REFERENCE_COMPARTMENT", "DATUM_REFERENCE_ELEMENT", "GENERAL_DATUM_REFERENCE"] {
345 if let Some(args) = entity.find(keyword) {
346 let (base, modifiers) = if args.len() >= 6 {
349 (args.get(4), args.get(5))
350 } else {
351 (args.first(), args.get(1))
352 };
353 let base_ref = match base {
354 Some(Value::Ref(id)) => *id,
355 Some(Value::Typed(_, inner)) => inner.first()?.as_ref_id().ok()?,
356 _ => return None,
357 };
358 let (letter, _) = datum_letter(entities, base_ref, depth + 1)?;
359 let modifier = modifiers
360 .and_then(|value| value.as_list().ok())
361 .map(|items| items.iter().filter_map(modifier_text).next().unwrap_or_default())
362 .unwrap_or_default();
363 return Some((letter, modifier));
364 }
365 }
366 None
367}
368
369fn modifier_text(value: &Value) -> Option<String> {
370 let name = match value {
371 Value::Enum(name) => name.clone(),
372 Value::Typed(_, inner) => match inner.first()? {
373 Value::Enum(name) => name.clone(),
374 _ => return None,
375 },
376 _ => return None,
377 };
378 match name.as_str() {
379 "MAXIMUM_MATERIAL_REQUIREMENT" => Some("MMC".into()),
380 "LEAST_MATERIAL_REQUIREMENT" => Some("LMC".into()),
381 _ => None,
382 }
383}
384
385struct DimensionValues {
388 nominal: Option<f64>,
389 upper_limit: Option<f64>,
390 lower_limit: Option<f64>,
391 plus_minus: Option<(f64, f64)>,
392 callout: Option<String>,
394 angle: bool,
395}
396
397fn dimension_values(entities: &HashMap<usize, Entity>, dimension: usize, names: &NameMap) -> DimensionValues {
398 let mut values = DimensionValues {
399 nominal: None,
400 upper_limit: None,
401 lower_limit: None,
402 plus_minus: None,
403 callout: None,
404 angle: false,
405 };
406 let scale = |entity: &Entity, value: f64| -> f64 {
407 if entity.has("PLANE_ANGLE_MEASURE_WITH_UNIT") {
408 value * names.angle_to_degrees
409 } else {
410 value * names.length_scale
411 }
412 };
413 for entity in entities.values() {
414 if let Some(args) = entity.find("DIMENSIONAL_CHARACTERISTIC_REPRESENTATION") {
415 if arg_ref(args, 0) != Some(dimension) {
416 continue;
417 }
418 let Some(representation) = arg_ref(args, 1).and_then(|id| entities.get(&id)) else {
419 continue;
420 };
421 let Some(rep_args) = representation
422 .find("SHAPE_DIMENSION_REPRESENTATION")
423 .or_else(|| representation.find("REPRESENTATION"))
424 else {
425 continue;
426 };
427 for item_ref in arg_refs(rep_args, 1) {
428 let Some(item) = entities.get(&item_ref) else { continue };
429 let name = item
430 .find("REPRESENTATION_ITEM")
431 .map(|args| arg_string(args, 0))
432 .unwrap_or_default();
433 if let Some(args) = item.find("DESCRIPTIVE_REPRESENTATION_ITEM") {
434 if arg_string(args, 0) == "hole callout" {
435 values.callout = Some(arg_string(args, 1));
436 }
437 continue;
438 }
439 let Some(raw) = item
440 .find("MEASURE_WITH_UNIT")
441 .and_then(|args| args.first())
442 .and_then(measure_value)
443 else {
444 continue;
445 };
446 if item.has("PLANE_ANGLE_MEASURE_WITH_UNIT") {
447 values.angle = true;
448 }
449 let value = scale(item, raw);
450 match name.as_str() {
451 "upper limit" => values.upper_limit = Some(value),
452 "lower limit" => values.lower_limit = Some(value),
453 _ => {
454 if values.nominal.is_none() || name == "nominal value" {
455 values.nominal = Some(value);
456 }
457 }
458 }
459 }
460 }
461 if let Some(args) = entity.find("PLUS_MINUS_TOLERANCE") {
462 if arg_ref(args, 1) != Some(dimension) {
463 continue;
464 }
465 let Some(range) = arg_ref(args, 0).and_then(|id| entities.get(&id)) else {
466 continue;
467 };
468 let Some(range_args) = range.find("TOLERANCE_VALUE") else {
469 continue;
470 };
471 let bound = |slot: usize| -> Option<f64> {
472 let measure = arg_ref(range_args, slot).and_then(|id| entities.get(&id))?;
473 let raw = measure
474 .find("LENGTH_MEASURE_WITH_UNIT")
475 .or_else(|| measure.find("PLANE_ANGLE_MEASURE_WITH_UNIT"))
476 .or_else(|| measure.find("MEASURE_WITH_UNIT"))
477 .and_then(|args| args.first())
478 .and_then(measure_value)?;
479 Some(scale(measure, raw))
480 };
481 if let (Some(lower), Some(upper)) = (bound(0), bound(1)) {
482 values.plus_minus = Some((lower, upper));
483 }
484 }
485 }
486 values
487}
488
489fn apply_values(params: &mut serde_json::Map<String, serde_json::Value>, values: &DimensionValues) {
491 if let (Some(nominal), Some(upper), Some(lower)) = (values.nominal, values.upper_limit, values.lower_limit) {
492 params.insert("tolMode".into(), "limits".into());
493 params.insert("tolUpper".into(), serde_json::json!(round6(upper - nominal)));
494 params.insert("tolLower".into(), serde_json::json!(round6(nominal - lower)));
495 } else if let Some((lower, upper)) = values.plus_minus {
496 if (lower.abs() - upper.abs()).abs() < 1e-9 {
497 params.insert("tolMode".into(), "symmetric".into());
498 params.insert("tolUpper".into(), serde_json::json!(round6(upper.abs())));
499 } else {
500 params.insert("tolMode".into(), "deviation".into());
501 params.insert("tolUpper".into(), serde_json::json!(round6(upper.abs())));
502 params.insert("tolLower".into(), serde_json::json!(round6(lower.abs())));
503 }
504 }
505}
506
507fn round6(value: f64) -> f64 {
508 (value * 1e6).round() / 1e6
509}
510
511struct ViewIn {
513 name: String,
514 camera: Option<PmiCamera>,
515 callouts: Vec<usize>,
517}
518
519fn read_camera(entities: &HashMap<usize, Entity>, resolver: &Resolver, camera_ref: usize) -> Option<(String, PmiCamera)> {
520 let camera = entities.get(&camera_ref)?;
521 let args = camera.find("CAMERA_MODEL_D3")?;
522 let name = arg_string(args, 0);
523 let frame = resolver.placement(arg_ref(args, 1)?).ok()?;
524 let volume = entities.get(&arg_ref(args, 2)?)?;
525 let volume_args = volume.find("VIEW_VOLUME")?;
526 let parallel = volume_args.first().map(|v| v.enum_is("PARALLEL")).unwrap_or(true);
527 let distance = resolver.length(volume_args.get(2)?.as_real().ok()?).max(1e-6);
528 let window = entities.get(&arg_ref(volume_args, 8)?)?;
529 let window_args = window.find("PLANAR_BOX")?;
530 let width = resolver.length(window_args.get(1)?.as_real().ok()?);
531 let height = resolver.length(window_args.get(2)?.as_real().ok()?).max(1e-9);
532 let eye = frame.origin;
533 let target = eye.add(frame.z.scale(distance));
534 let projection = if parallel {
535 PmiProjection::Orthographic {
536 half_height: height * 0.5,
537 }
538 } else {
539 PmiProjection::Perspective {
540 fov_y_deg: 2.0 * (height * 0.5 / distance).atan().to_degrees(),
541 }
542 };
543 let aspect = if height > 0.0 { width / height } else { 1.5 };
544 Some((
545 name,
546 PmiCamera {
547 eye: [eye.x, eye.y, eye.z],
548 target: [target.x, target.y, target.z],
549 up: [frame.y.x, frame.y.y, frame.y.z],
550 projection,
551 viewport: [800.0 * aspect, 800.0],
552 },
553 ))
554}
555
556fn expand_item(entities: &HashMap<usize, Entity>, item: usize, out: &mut Vec<usize>) {
558 let Some(entity) = entities.get(&item) else { return };
559 if let Some(args) = entity.find("ANNOTATION_PLANE") {
560 for element in arg_refs(args, 3) {
561 expand_item(entities, element, out);
562 }
563 } else if entity.has("DRAUGHTING_CALLOUT") || entity.has("ANNOTATION_OCCURRENCE") || entity.has("ANNOTATION_CURVE_OCCURRENCE") || entity.has("ANNOTATION_TEXT_OCCURRENCE") {
564 out.push(item);
565 }
566}
567
568fn plane_of_callouts(entities: &HashMap<usize, Entity>) -> HashMap<usize, usize> {
571 let mut planes: Vec<(&usize, &Entity)> = entities.iter().filter(|(_, e)| e.has("ANNOTATION_PLANE")).collect();
572 planes.sort_by_key(|(id, _)| **id);
573 let mut out: HashMap<usize, usize> = HashMap::default();
574 for (id, entity) in planes {
575 let Some(args) = entity.find("ANNOTATION_PLANE") else { continue };
576 let mut members = Vec::new();
577 for element in arg_refs(args, 3) {
578 expand_item(entities, element, &mut members);
579 }
580 for member in members {
581 out.entry(member).or_insert(*id);
582 }
583 }
584 out
585}
586
587fn plane_face_name(
592 entities: &HashMap<usize, Entity>,
593 resolver: &Resolver,
594 names: &NameMap,
595 plane: usize,
596) -> Option<String> {
597 let args = entities.get(&plane)?.find("ANNOTATION_PLANE")?;
598 let item = arg_ref(args, 2)?;
599 let placement = match entities.get(&item) {
600 Some(entity) if entity.has("PLANE") => arg_ref(entity.find("PLANE")?, 1)?,
601 Some(entity) if entity.has("AXIS2_PLACEMENT_3D") => item,
602 _ => return None,
603 };
604 let frame = resolver.placement(placement).ok()?;
605 let normal = frame.z.normalized().ok()?;
606 let tolerance = 1e-4 * frame.origin.length().max(1.0);
607 let mut faces: Vec<(&usize, &String)> = names.faces.iter().collect();
608 faces.sort();
609 for (face_id, name) in faces {
610 let Some(face_args) = entities.get(face_id).and_then(|e| e.find("ADVANCED_FACE")) else { continue };
611 let Some(surface) = arg_ref(face_args, 2).and_then(|id| entities.get(&id)) else { continue };
612 let Some(plane_args) = surface.find("PLANE") else { continue };
613 let Some(face_frame) = arg_ref(plane_args, 1).and_then(|p| resolver.placement(p).ok()) else { continue };
614 let Ok(face_normal) = face_frame.z.normalized() else { continue };
615 if face_normal.dot(normal).abs() < 1.0 - 1e-6 {
616 continue;
617 }
618 if face_frame.origin.sub(frame.origin).dot(normal).abs() > tolerance {
619 continue;
620 }
621 return Some(name.clone());
622 }
623 None
624}
625
626#[derive(Default)]
630struct ValidationProps {
631 unicode: HashMap<usize, String>,
632 centre: HashMap<usize, [f64; 3]>,
633}
634
635impl ValidationProps {
636 fn read(entities: &HashMap<usize, Entity>, resolver: &Resolver) -> Self {
637 let mut out = Self::default();
638 for entity in entities.values() {
639 let Some(args) = entity.find("PROPERTY_DEFINITION_REPRESENTATION") else { continue };
640 let (Some(definition), Some(representation)) = (arg_ref(args, 0), arg_ref(args, 1)) else { continue };
641 let Some(within) = entities
642 .get(&definition)
643 .and_then(|e| e.find("PROPERTY_DEFINITION"))
644 .and_then(|a| arg_ref(a, 2))
645 else {
646 continue;
647 };
648 let Some(item) = entities
649 .get(&within)
650 .and_then(|e| e.find("CHARACTERIZED_ITEM_WITHIN_REPRESENTATION"))
651 .and_then(|a| arg_ref(a, 2))
652 else {
653 continue;
654 };
655 let Some(items) = entities.get(&representation).and_then(|e| e.find("REPRESENTATION")) else { continue };
656 for value in arg_refs(items, 1) {
657 let Some(value_entity) = entities.get(&value) else { continue };
658 if let Some(dri) = value_entity.find("DESCRIPTIVE_REPRESENTATION_ITEM") {
659 if arg_string(dri, 0) == "equivalent unicode string" {
660 out.unicode.entry(item).or_insert_with(|| arg_string(dri, 1));
661 }
662 }
663 if let Some(point) = value_entity.find("CARTESIAN_POINT") {
664 if arg_string(point, 0) == "polyline centre point" {
665 if let Ok(p) = resolver.point(value) {
666 out.centre.entry(item).or_insert([p.x, p.y, p.z]);
667 }
668 }
669 }
670 }
671 }
672 out
673 }
674}
675
676fn callout_label(
682 entities: &HashMap<usize, Entity>,
683 resolver: &Resolver,
684 props: &ValidationProps,
685 callout: usize,
686) -> (Option<[f64; 3]>, String) {
687 let mut text = props.unicode.get(&callout).cloned().unwrap_or_default();
688 let mut position = entities
689 .get(&callout)
690 .and_then(|e| e.find("DRAUGHTING_CALLOUT"))
691 .map(|args| arg_refs(args, 1))
692 .unwrap_or_default()
693 .iter()
694 .find_map(|subset| props.centre.get(subset).copied())
695 .or_else(|| props.centre.get(&callout).copied());
696 let mut fallback = None;
697 let mut visit = |id: usize| {
698 let Some(entity) = entities.get(&id) else { return };
699 if let Some(args) = entity.find("TEXT_LITERAL") {
700 if text.is_empty() {
701 text = arg_string(args, 0);
702 }
703 if position.is_none() {
704 if let Some(frame) = arg_ref(args, 1).and_then(|p| resolver.placement(p).ok()) {
705 position = Some([frame.origin.x, frame.origin.y, frame.origin.z]);
706 }
707 }
708 }
709 if let Some(args) = entity.find("POLYLINE") {
710 if fallback.is_none() {
711 if let Some(point) = arg_refs(args, 1).first().and_then(|p| resolver.point(*p).ok()) {
712 fallback = Some([point.x, point.y, point.z]);
713 }
714 }
715 }
716 };
717 let mut queue = vec![callout];
718 let mut seen: HashSet<usize> = HashSet::default();
719 while let Some(id) = queue.pop() {
720 if !seen.insert(id) {
721 continue;
722 }
723 visit(id);
724 let Some(entity) = entities.get(&id) else { continue };
725 for (keyword, slot) in [("DRAUGHTING_CALLOUT", 1usize), ("ANNOTATION_CURVE_OCCURRENCE", 2), ("ANNOTATION_TEXT_OCCURRENCE", 2), ("ANNOTATION_OCCURRENCE", 2), ("GEOMETRIC_CURVE_SET", 1)] {
726 if let Some(args) = entity.find(keyword) {
727 match args.get(slot) {
728 Some(Value::Ref(target)) => queue.push(*target),
729 Some(Value::List(items)) => queue.extend(items.iter().filter_map(|v| v.as_ref_id().ok())),
730 _ => {}
731 }
732 }
733 }
734 }
735 (position.or(fallback), text)
736}
737
738pub fn read_step_pmi(text: &str, feature_name: &str) -> Result<Option<PmiState>, String> {
742 if !text.contains("DIMENSIONAL_")
743 && !text.contains("_TOLERANCE(")
744 && !text.contains("DATUM(")
745 && !text.contains("CAMERA_MODEL_D3(")
746 && !text.contains("DRAUGHTING_CALLOUT(")
747 {
748 return Ok(None);
749 }
750 let entities = parse_data_section(text)?;
751 let names = NameMap::build(text, &entities, feature_name)?;
752 let resolver = Resolver {
753 entities: &entities,
754 length_scale: names.length_scale,
755 };
756 let aspects = aspect_names(&entities, &names);
757 let aspect_name = |id: usize| -> Option<String> { aspects.get(&id).cloned() };
758 let mut ids: BTreeMap<usize, &Entity> = entities.iter().map(|(id, entity)| (*id, entity)).collect();
759 let mut state = PmiState::default();
760 let mut semantics: Vec<Semantic> = Vec::new();
761 let mut params = |pairs: Vec<(&str, serde_json::Value)>| -> serde_json::Map<String, serde_json::Value> {
762 let mut map = serde_json::Map::new();
763 for (key, value) in pairs {
764 map.insert(key.to_string(), value);
765 }
766 map
767 };
768
769 let mut datum_ids: HashMap<usize, String> = HashMap::default();
771 for (id, entity) in ids.iter() {
772 let Some(args) = entity.find("DATUM") else { continue };
773 let letter = arg_string(args, 4).to_ascii_uppercase();
774 if letter.is_empty() {
775 continue;
776 }
777 let feature = entities.values().find_map(|candidate| {
779 let rel = candidate.find("SHAPE_ASPECT_RELATIONSHIP")?;
780 (arg_ref(rel, 3) == Some(*id)).then(|| arg_ref(rel, 2)).flatten()
781 });
782 let Some(target) = feature.and_then(aspect_name) else {
783 continue;
784 };
785 let annotation_id = state.next_id("DTM");
786 datum_ids.insert(*id, letter.clone());
787 semantics.push(Semantic {
788 entity: *id,
789 annotation: PmiAnnotation {
790 kind: "datum".into(),
791 enabled: true,
792 params: serde_json::Value::Object(params(vec![
793 ("id", annotation_id.into()),
794 ("target", target.into()),
795 ("letter", letter.into()),
796 ])),
797 label_world: None,
798 },
799 });
800 }
801
802 for (id, entity) in ids.iter() {
804 let mut fields: Option<(&str, serde_json::Map<String, serde_json::Value>)> = None;
805 if let Some(args) = entity.find("ANGULAR_LOCATION").or_else(|| entity.find("DIMENSIONAL_LOCATION")) {
806 let (Some(a), Some(b)) = (arg_ref(args, 2).and_then(aspect_name), arg_ref(args, 3).and_then(aspect_name)) else {
807 continue;
808 };
809 let values = dimension_values(&entities, *id, &names);
810 if entity.has("ANGULAR_LOCATION") {
811 let mut map = params(vec![("targets", serde_json::json!([a, b])), ("decimals", 1.into())]);
812 if let Some(nominal) = values.nominal {
813 let kind = if nominal > 180.0 { "reflex" } else if nominal > 90.0 + 1e-9 { "obtuse" } else { "acute" };
814 map.insert("angleType".into(), kind.into());
815 }
816 apply_values(&mut map, &values);
817 fields = Some(("angle", map));
818 } else {
819 let mut map = params(vec![("targets", serde_json::json!([a, b]))]);
820 if let Some(orientation) = orientation_axis(&entities, *id) {
821 map.insert("alignment".into(), orientation.into());
822 }
823 apply_values(&mut map, &values);
824 fields = Some(("linear", map));
825 }
826 } else if let Some(args) = entity.find("DIMENSIONAL_SIZE") {
827 let Some(target) = arg_ref(args, 0).and_then(aspect_name) else {
828 continue;
829 };
830 let name = arg_string(args, 1).to_ascii_lowercase();
831 let values = dimension_values(&entities, *id, &names);
832 if let Some(callout) = &values.callout {
833 let _ = callout;
834 let map = params(vec![("target", target.into()), ("showQuantity", true.into())]);
835 fields = Some(("holeCallout", map));
836 } else if name.contains("diameter") || name.contains("radius") {
837 let mut map = params(vec![
838 ("target", target.into()),
839 ("displayStyle", if name.contains("radius") { "radius" } else { "diameter" }.into()),
840 ]);
841 apply_values(&mut map, &values);
842 fields = Some(("radial", map));
843 } else {
844 let mut map = params(vec![("targets", serde_json::json!([target]))]);
845 apply_values(&mut map, &values);
846 fields = Some(("linear", map));
847 }
848 }
849 if let Some((kind, mut map)) = fields {
850 let prefix = match kind {
851 "angle" => "ANG",
852 "radial" => "RAD",
853 "holeCallout" => "HOLE",
854 _ => "DIM",
855 };
856 map.insert("id".into(), state.next_id(prefix).into());
857 semantics.push(Semantic {
858 entity: *id,
859 annotation: PmiAnnotation {
860 kind: kind.into(),
861 enabled: true,
862 params: serde_json::Value::Object(map),
863 label_world: None,
864 },
865 });
866 }
867 }
868
869 for (id, entity) in ids.iter() {
871 let Some((kind_record, base_args)) = entity.records.iter().find_map(|(keyword, args)| {
872 characteristic_for_entity(keyword).map(|c| (c, args))
873 }) else {
874 continue;
875 };
876 let args = entity.find("GEOMETRIC_TOLERANCE").unwrap_or(base_args);
879 if args.len() < 4 {
880 continue;
881 }
882 let Some(target) = arg_ref(args, 3).and_then(aspect_name) else {
883 continue;
884 };
885 let magnitude = arg_ref(args, 2)
886 .and_then(|m| entities.get(&m))
887 .and_then(|m| m.find("LENGTH_MEASURE_WITH_UNIT").or_else(|| m.find("MEASURE_WITH_UNIT")))
888 .and_then(|a| a.first())
889 .and_then(measure_value)
890 .map(|v| v * names.length_scale)
891 .unwrap_or(0.1);
892 let mut map = params(vec![
893 ("target", target.into()),
894 ("characteristic", kind_record.id.into()),
895 ("zoneValue", serde_json::json!(round6(magnitude))),
896 ]);
897 if let Some(mod_args) = entity.find("GEOMETRIC_TOLERANCE_WITH_MODIFIERS") {
898 if let Some(items) = mod_args.first().and_then(|v| v.as_list().ok()) {
899 if let Some(modifier) = items.iter().filter_map(modifier_text).next() {
900 map.insert("materialCondition".into(), modifier.into());
901 }
902 }
903 }
904 if let Some(ref_args) = entity.find("GEOMETRIC_TOLERANCE_WITH_DATUM_REFERENCE") {
905 let mut letters: Vec<(String, String)> = Vec::new();
906 for system_ref in arg_refs(ref_args, 0) {
907 let Some(system) = entities.get(&system_ref) else { continue };
908 if let Some(system_args) = system.find("DATUM_SYSTEM") {
909 for constituent in arg_refs(system_args, 4) {
910 if let Some(pair) = datum_letter(&entities, constituent, 0) {
911 letters.push(pair);
912 }
913 }
914 } else if let Some(pair) = datum_letter(&entities, system_ref, 0) {
915 letters.push(pair);
916 }
917 }
918 for (slot, (letter, modifier)) in ["A", "B", "C"].iter().zip(letters) {
919 map.insert(format!("datum{slot}"), letter.into());
920 if !modifier.is_empty() {
921 map.insert(format!("datum{slot}Modifier"), modifier.into());
922 }
923 }
924 }
925 let cylindrical = entities.values().any(|zone| {
926 zone.find("TOLERANCE_ZONE")
927 .map(|zone_args| {
928 arg_refs(zone_args, 4).contains(id)
929 && arg_ref(zone_args, 5)
930 .and_then(|form| entities.get(&form))
931 .and_then(|form| form.find("TOLERANCE_ZONE_FORM"))
932 .map(|form_args| arg_string(form_args, 0).to_ascii_lowercase().contains("cylindrical"))
933 .unwrap_or(false)
934 })
935 .unwrap_or(false)
936 });
937 if cylindrical {
938 map.insert("zoneDiameter".into(), true.into());
939 }
940 map.insert("id".into(), state.next_id("FCF").into());
941 semantics.push(Semantic {
942 entity: *id,
943 annotation: PmiAnnotation {
944 kind: "fcf".into(),
945 enabled: true,
946 params: serde_json::Value::Object(map),
947 label_world: None,
948 },
949 });
950 }
951 ids.clear();
952
953 let mut callout_of: HashMap<usize, usize> = HashMap::default();
955 for entity in entities.values() {
956 let Some(args) = entity.find("DRAUGHTING_MODEL_ITEM_ASSOCIATION") else { continue };
957 if let (Some(definition), Some(callout)) = (arg_ref(args, 2), arg_ref(args, 4)) {
958 callout_of.entry(definition).or_insert(callout);
959 }
960 }
961 let datum_feature_of: HashMap<usize, usize> = entities
963 .values()
964 .filter_map(|entity| {
965 let rel = entity.find("SHAPE_ASPECT_RELATIONSHIP")?;
966 Some((arg_ref(rel, 3)?, arg_ref(rel, 2)?))
967 })
968 .collect();
969 let mut linked_callouts: HashSet<usize> = HashSet::default();
970 let mut annotations: Vec<(Option<usize>, PmiAnnotation)> = Vec::new();
971 let plane_of = plane_of_callouts(&entities);
975 let props = ValidationProps::read(&entities, &resolver);
976 let mut plane_faces: HashMap<usize, Option<String>> = HashMap::default();
977 let mut plane_param = |callout: usize, annotation: &mut PmiAnnotation| {
978 let Some(plane) = plane_of.get(&callout).copied() else { return };
979 let face = plane_faces
980 .entry(plane)
981 .or_insert_with(|| plane_face_name(&entities, &resolver, &names, plane))
982 .clone();
983 if let (Some(face), Some(object)) = (face, annotation.params.as_object_mut()) {
984 object.insert("plane".into(), serde_json::Value::String(face));
985 }
986 };
987 for semantic in semantics {
988 let callout = callout_of
989 .get(&semantic.entity)
990 .copied()
991 .or_else(|| datum_feature_of.get(&semantic.entity).and_then(|f| callout_of.get(f)).copied());
992 let mut annotation = semantic.annotation;
993 if let Some(callout) = callout {
994 linked_callouts.insert(callout);
995 let (position, _) = callout_label(&entities, &resolver, &props, callout);
996 annotation.label_world = position;
997 plane_param(callout, &mut annotation);
998 }
999 annotations.push((callout, annotation));
1000 }
1001 for (id, entity) in entities.iter() {
1003 if !entity.has("DRAUGHTING_CALLOUT") || linked_callouts.contains(id) {
1004 continue;
1005 }
1006 let (position, text) = callout_label(&entities, &resolver, &props, *id);
1007 if text.trim().is_empty() {
1008 continue;
1009 }
1010 let annotation_id = state.next_id("NOTE");
1011 let mut annotation = PmiAnnotation {
1012 kind: "note".into(),
1013 enabled: true,
1014 params: serde_json::Value::Object(params(vec![("id", annotation_id.into()), ("text", text.into())])),
1015 label_world: position,
1016 };
1017 plane_param(*id, &mut annotation);
1018 annotations.push((Some(*id), annotation));
1019 }
1020 if annotations.is_empty() && !text.contains("CAMERA_MODEL_D3(") {
1021 return Ok(None);
1022 }
1023
1024 let mut views_in: Vec<ViewIn> = Vec::new();
1026 let mut models: Vec<(&usize, &Entity)> = entities.iter().filter(|(_, e)| e.has("DRAUGHTING_MODEL")).collect();
1027 models.sort_by_key(|(id, _)| **id);
1028 for (_, model) in models {
1029 let Some(args) = model.find("DRAUGHTING_MODEL") else { continue };
1030 let items = arg_refs(args, 1);
1031 let camera = items
1032 .iter()
1033 .find_map(|item| read_camera(&entities, &resolver, *item));
1034 let Some((camera_name, camera)) = camera else {
1035 continue; };
1037 let mut callouts = Vec::new();
1038 for item in &items {
1039 expand_item(&entities, *item, &mut callouts);
1040 }
1041 let model_name = arg_string(args, 0);
1042 views_in.push(ViewIn {
1043 name: if camera_name.trim().is_empty() { model_name } else { camera_name },
1044 camera: Some(camera),
1045 callouts,
1046 });
1047 }
1048 let mut placed: HashSet<usize> = HashSet::default();
1049 for view_in in views_in {
1050 let id = state.next_id("VIEW");
1051 let mut view = PmiView {
1052 id,
1053 name: view_in.name,
1054 camera: view_in.camera,
1055 display: PmiDisplay::default(),
1056 annotations: Vec::new(),
1057 };
1058 for (index, (callout, annotation)) in annotations.iter().enumerate() {
1059 if let Some(callout) = callout {
1060 if view_in.callouts.contains(callout) && !placed.contains(&index) {
1061 view.annotations.push(annotation.clone());
1062 placed.insert(index);
1063 }
1064 }
1065 }
1066 state.views.push(view);
1067 }
1068 let unplaced: Vec<PmiAnnotation> = annotations
1069 .iter()
1070 .enumerate()
1071 .filter(|(index, _)| !placed.contains(index))
1072 .map(|(_, (_, annotation))| annotation.clone())
1073 .collect();
1074 if !unplaced.is_empty() {
1075 let id = state.next_id("VIEW");
1076 state.views.push(PmiView {
1077 id,
1078 name: "Imported PMI".into(),
1079 camera: None,
1080 display: PmiDisplay::default(),
1081 annotations: unplaced,
1082 });
1083 }
1084 if state.views.is_empty() {
1085 return Ok(None);
1086 }
1087 Ok(Some(state))
1088}
1089
1090fn orientation_axis(entities: &HashMap<usize, Entity>, dimension: usize) -> Option<&'static str> {
1092 for entity in entities.values() {
1093 let Some(args) = entity.find("DIMENSIONAL_CHARACTERISTIC_REPRESENTATION") else { continue };
1094 if arg_ref(args, 0) != Some(dimension) {
1095 continue;
1096 }
1097 let Some(representation) = arg_ref(args, 1).and_then(|id| entities.get(&id)) else { continue };
1098 let Some(rep_args) = representation.find("SHAPE_DIMENSION_REPRESENTATION") else { continue };
1099 for item_ref in arg_refs(rep_args, 1) {
1100 let Some(item) = entities.get(&item_ref) else { continue };
1101 let Some(placement) = item.find("AXIS2_PLACEMENT_3D") else { continue };
1102 if arg_string(placement, 0) != "orientation" {
1103 continue;
1104 }
1105 let Some(direction) = arg_ref(placement, 3).and_then(|id| entities.get(&id)) else { continue };
1106 let Some(list) = direction.find("DIRECTION").and_then(|d| d.get(1)).and_then(|v| v.as_list().ok()) else { continue };
1107 let components: Vec<f64> = list.iter().filter_map(|v| v.as_real().ok()).collect();
1108 let axis = components
1109 .iter()
1110 .enumerate()
1111 .max_by(|a, b| a.1.abs().partial_cmp(&b.1.abs()).unwrap_or(std::cmp::Ordering::Equal))
1112 .map(|(index, _)| index)?;
1113 return Some(["X", "Y", "Z"][axis.min(2)]);
1114 }
1115 }
1116 None
1117}