1use super::{
42 id_list, real, write_direction, write_placement, write_point, StepItemOwner, StepWriter,
43};
44use crate::feature_pipeline::pmi::annotations::fcf::{characteristic, datum_references};
45use crate::feature_pipeline::pmi::layout::{present, LayoutStyle};
46use crate::feature_pipeline::pmi::{
47 PmiAnnotation, PmiGeometry, PmiProjection, PmiReport, PmiState, PmiStatus,
48 ToleranceBlock, ToleranceMode,
49};
50use crate::feature_pipeline::Env;
51use crate::Vec3;
52use rustc_hash::FxHashMap as HashMap;
53
54pub struct StepPmi<'a> {
56 pub state: &'a PmiState,
57 pub report: &'a PmiReport,
58}
59
60pub(crate) struct StepContext<'a> {
62 pub product_shape: usize,
63 pub representation: usize,
65 pub geometry_context: usize,
66 pub length_unit: usize,
67 pub angle_unit: usize,
68 pub faces: &'a HashMap<String, (usize, StepItemOwner)>,
72 pub edges: &'a HashMap<String, (usize, StepItemOwner)>,
73 pub vertices: &'a HashMap<String, (StepItemOwner, Vec<(Vec3, usize)>)>,
77}
78
79const MM_PER_POINT: f64 = 25.4 / 72.0;
81const LINE_WIDTH: f64 = 0.13;
83
84pub(crate) fn step_text(value: &str) -> String {
87 let mut out = String::new();
88 let mut run: Vec<u16> = Vec::new();
89 let flush = |run: &mut Vec<u16>, out: &mut String| {
90 if !run.is_empty() {
91 out.push_str("\\X2\\");
92 for unit in run.iter() {
93 out.push_str(&format!("{unit:04X}"));
94 }
95 out.push_str("\\X0\\");
96 run.clear();
97 }
98 };
99 for ch in value.replace('\n', " / ").chars() {
100 if ch.is_ascii() && !ch.is_ascii_control() {
101 flush(&mut run, &mut out);
102 if ch == '\'' {
103 out.push_str("''");
104 } else if ch == '\\' {
105 out.push_str("\\\\");
106 } else {
107 out.push(ch);
108 }
109 } else if !ch.is_ascii() {
110 let mut units = [0u16; 2];
111 for unit in ch.encode_utf16(&mut units) {
112 run.push(*unit);
113 }
114 }
115 }
116 flush(&mut run, &mut out);
117 out
118}
119
120#[derive(Clone, Copy)]
123struct Item {
124 entity: usize,
125 owner: StepItemOwner,
126}
127
128struct Emitter<'a, 'b> {
129 writer: &'a mut StepWriter,
130 context: &'a StepContext<'b>,
131 aspects: HashMap<String, usize>,
134 datums: HashMap<String, usize>,
136 unresolved: std::collections::BTreeSet<String>,
140 null_style: usize,
141 curve_style: usize,
142}
143
144impl Emitter<'_, '_> {
145 fn add(&mut self, body: impl Into<String>) -> usize {
146 self.writer.add(body)
147 }
148
149 fn item(&mut self, name: &str) -> Option<Item> {
152 let found = self.lookup(name);
153 if found.is_none() {
154 self.unresolved.insert(name.to_string());
155 }
156 found
157 }
158
159 fn lookup(&self, name: &str) -> Option<Item> {
161 if let Some((solid, coords)) = name.split_once('@') {
162 let mut parts = coords.split(',').map(|part| part.trim().parse::<f64>().ok());
163 let (x, y, z) = (parts.next()??, parts.next()??, parts.next()??);
164 let query = Vec3::new(x, y, z);
165 let (owner, points) = self.context.vertices.get(solid)?;
166 let mut best: Option<(f64, usize)> = None;
167 for (point, id) in points {
168 let distance = point.sub(query).length();
169 if best.map(|(d, _)| distance < d).unwrap_or(true) {
170 best = Some((distance, *id));
171 }
172 }
173 return best
174 .filter(|(d, _)| *d < 1e-6 + 1e-9 * query.length())
175 .map(|(_, entity)| Item {
176 entity,
177 owner: *owner,
178 });
179 }
180 if let Some((entity, owner)) = self.context.faces.get(name) {
181 return Some(Item {
182 entity: *entity,
183 owner: *owner,
184 });
185 }
186 if let Some((entity, owner)) = self.context.edges.get(name) {
187 return Some(Item {
188 entity: *entity,
189 owner: *owner,
190 });
191 }
192 None
193 }
194
195 fn aspect(&mut self, name: &str) -> Option<usize> {
197 if let Some(id) = self.aspects.get(name) {
198 return Some(*id);
199 }
200 let item = self.item(name)?;
201 let aspect = self.add(format!(
202 "SHAPE_ASPECT('{}','',#{},.T.)",
203 step_text(name),
204 item.owner.product_shape
205 ));
206 self.link_aspect(aspect, item, "");
207 self.add(format!("ID_ATTRIBUTE('{}',#{aspect})", step_text(name)));
208 self.aspects.insert(name.to_string(), aspect);
209 Some(aspect)
210 }
211
212 fn link_aspect(&mut self, aspect: usize, item: Item, description: &str) {
213 let target = item.entity;
214 self.add(format!(
215 "GEOMETRIC_ITEM_SPECIFIC_USAGE('','{}',#{aspect},#{},#{target})",
216 step_text(description),
217 item.owner.representation
218 ));
219 }
220
221 fn length_measure(&mut self, value: f64) -> Result<usize, String> {
222 Ok(self.add(format!(
223 "LENGTH_MEASURE_WITH_UNIT(LENGTH_MEASURE({}),#{})",
224 real(value)?,
225 self.context.length_unit
226 )))
227 }
228
229 fn angle_measure(&mut self, degrees: f64) -> Result<usize, String> {
230 Ok(self.add(format!(
231 "PLANE_ANGLE_MEASURE_WITH_UNIT(PLANE_ANGLE_MEASURE({}),#{})",
232 real(degrees.to_radians())?,
233 self.context.angle_unit
234 )))
235 }
236
237 fn measure_item(&mut self, name: &str, value: f64, angle: bool) -> Result<usize, String> {
239 Ok(if angle {
240 self.add(format!(
241 "(MEASURE_REPRESENTATION_ITEM()MEASURE_WITH_UNIT(PLANE_ANGLE_MEASURE({}),#{})PLANE_ANGLE_MEASURE_WITH_UNIT()REPRESENTATION_ITEM('{name}'))",
242 real(value.to_radians())?,
243 self.context.angle_unit
244 ))
245 } else {
246 self.add(format!(
247 "(LENGTH_MEASURE_WITH_UNIT()MEASURE_REPRESENTATION_ITEM()MEASURE_WITH_UNIT(LENGTH_MEASURE({}),#{})REPRESENTATION_ITEM('{name}'))",
248 real(value)?,
249 self.context.length_unit
250 ))
251 })
252 }
253
254 fn dimension_values(
256 &mut self,
257 dimension: usize,
258 value: f64,
259 angle: bool,
260 tolerance: &ToleranceBlock,
261 is_reference: bool,
262 extra_items: &[usize],
263 ) -> Result<(), String> {
264 let mut items = vec![self.measure_item("nominal value", value, angle)?];
265 let toleranced = !is_reference;
266 if toleranced && tolerance.mode == ToleranceMode::Limits {
267 items.push(self.measure_item("upper limit", value + tolerance.upper, angle)?);
268 items.push(self.measure_item("lower limit", value - tolerance.lower, angle)?);
269 }
270 items.extend_from_slice(extra_items);
271 let representation = self.add(format!(
272 "SHAPE_DIMENSION_REPRESENTATION('',{},#{})",
273 id_list(&items),
274 self.context.geometry_context
275 ));
276 self.add(format!(
277 "DIMENSIONAL_CHARACTERISTIC_REPRESENTATION(#{dimension},#{representation})"
278 ));
279 if toleranced {
280 if let (ToleranceMode::Symmetric | ToleranceMode::Deviation, Some((lower, upper))) =
281 (tolerance.mode, tolerance.bounds())
282 {
283 let (lower, upper) = if angle {
284 (self.angle_measure(lower)?, self.angle_measure(upper)?)
285 } else {
286 (self.length_measure(lower)?, self.length_measure(upper)?)
287 };
288 let range = self.add(format!("TOLERANCE_VALUE(#{lower},#{upper})"));
289 self.add(format!("PLUS_MINUS_TOLERANCE(#{range},#{dimension})"));
290 }
291 }
292 Ok(())
293 }
294
295 fn semantic(
298 &mut self,
299 annotation: &PmiAnnotation,
300 report: &crate::feature_pipeline::pmi::PmiAnnotationReport,
301 env: &Env,
302 ) -> Result<Option<usize>, String> {
303 let id = report.id.as_str();
304 let tolerance = ToleranceBlock::read(annotation, env).unwrap_or(ToleranceBlock {
305 mode: ToleranceMode::None,
306 upper: 0.0,
307 lower: 0.0,
308 });
309 let is_reference = annotation.flag("isReference");
310 let value = report.value.unwrap_or(0.0);
311 match &report.geometry {
312 PmiGeometry::Linear { a, b, component } => {
313 let aspects: Vec<usize> = report
314 .references
315 .iter()
316 .filter_map(|name| self.aspect(name))
317 .collect();
318 let dimension = match aspects.as_slice() {
319 [single] if report.references.len() == 1 => {
320 self.add(format!("DIMENSIONAL_SIZE(#{single},'curve length')"))
321 }
322 [first, second] => self.add(format!(
323 "DIMENSIONAL_LOCATION('linear distance','',#{first},#{second})"
324 )),
325 _ => return Ok(None),
326 };
327 self.add(format!("ID_ATTRIBUTE('{}',#{dimension})", step_text(id)));
328 let mut extra = Vec::new();
331 if let Some(axis) = component {
332 let x = match axis {
333 'X' => Vec3::new(1.0, 0.0, 0.0),
334 'Y' => Vec3::new(0.0, 1.0, 0.0),
335 _ => Vec3::new(0.0, 0.0, 1.0),
336 };
337 let z = x.perpendicular().unwrap_or(Vec3::new(0.0, 0.0, 1.0));
338 let origin = write_point(self.writer, Vec3::new(a[0], a[1], a[2]))?;
339 let z_dir = write_direction(self.writer, z)?;
340 let x_dir = write_direction(self.writer, x)?;
341 extra.push(self.add(format!(
342 "AXIS2_PLACEMENT_3D('orientation',#{origin},#{z_dir},#{x_dir})"
343 )));
344 }
345 let _ = b;
346 self.dimension_values(dimension, value, false, &tolerance, is_reference, &extra)?;
347 Ok(Some(dimension))
348 }
349 PmiGeometry::Radial { diameter, sphere, .. } => {
350 let Some(aspect) = report.references.first().and_then(|name| self.aspect(name)) else {
351 return Ok(None);
352 };
353 let name = match (diameter, sphere) {
354 (true, false) => "diameter",
355 (false, false) => "radius",
356 (true, true) => "spherical diameter",
357 (false, true) => "spherical radius",
358 };
359 let dimension = self.add(format!("DIMENSIONAL_SIZE(#{aspect},'{name}')"));
360 self.add(format!("ID_ATTRIBUTE('{}',#{dimension})", step_text(id)));
361 self.dimension_values(dimension, value, false, &tolerance, is_reference, &[])?;
362 Ok(Some(dimension))
363 }
364 PmiGeometry::Angular { degrees, .. } => {
365 let aspects: Vec<usize> = report
366 .references
367 .iter()
368 .filter_map(|name| self.aspect(name))
369 .collect();
370 let [first, second] = aspects.as_slice() else {
371 return Ok(None);
372 };
373 let selection = if *degrees > 180.0 { ".LARGE." } else { ".EQUAL." };
374 let dimension = self.add(format!(
375 "ANGULAR_LOCATION('angular location','',#{first},#{second},{selection})"
376 ));
377 self.add(format!("ID_ATTRIBUTE('{}',#{dimension})", step_text(id)));
378 self.dimension_values(dimension, *degrees, true, &tolerance, is_reference, &[])?;
379 Ok(Some(dimension))
380 }
381 PmiGeometry::Hole { .. } => {
382 let Some(aspect) = report.references.first().and_then(|name| self.aspect(name)) else {
383 return Ok(None);
384 };
385 let dimension = self.add(format!("DIMENSIONAL_SIZE(#{aspect},'diameter')"));
386 self.add(format!("ID_ATTRIBUTE('{}',#{dimension})", step_text(id)));
387 let callout = self.add(format!(
388 "DESCRIPTIVE_REPRESENTATION_ITEM('hole callout','{}')",
389 step_text(&report.text)
390 ));
391 let none = ToleranceBlock {
392 mode: ToleranceMode::None,
393 upper: 0.0,
394 lower: 0.0,
395 };
396 self.dimension_values(dimension, value, false, &none, false, &[callout])?;
397 Ok(Some(dimension))
398 }
399 PmiGeometry::Datum { letter, .. } => {
400 let _ = letter;
403 Ok(report.references.first().and_then(|name| self.aspects.get(name).copied()))
404 }
405 PmiGeometry::Fcf { frame, .. } => {
406 let Some(aspect) = report.references.first().and_then(|name| self.aspect(name)) else {
407 return Ok(None);
408 };
409 let Some(kind) = characteristic(&frame.characteristic) else {
410 return Ok(None);
411 };
412 let magnitude = self.length_measure(value)?;
413 let datums = datum_references(annotation);
415 let mut compartments = Vec::new();
416 for (letter, modifier) in &datums {
417 let Some(datum) = self.datums.get(letter).copied() else {
418 return Ok(None);
419 };
420 let modifiers = match material_condition(modifier) {
421 Some(condition) => format!("(SIMPLE_DATUM_REFERENCE_MODIFIER({condition}))"),
422 None => "$".into(),
423 };
424 compartments.push(self.add(format!(
425 "DATUM_REFERENCE_COMPARTMENT('','',#{},.F.,#{datum},{modifiers})",
426 self.context.product_shape
427 )));
428 }
429 let system = if compartments.is_empty() {
430 None
431 } else {
432 Some(self.add(format!(
433 "DATUM_SYSTEM('','',#{},.F.,{})",
434 self.context.product_shape,
435 id_list(&compartments)
436 )))
437 };
438 let condition = material_condition(annotation.text("materialCondition"));
439 let base = format!(
440 "GEOMETRIC_TOLERANCE('{}','',#{magnitude},#{aspect})",
441 step_text(id)
442 );
443 let tolerance = if system.is_none() && condition.is_none() {
444 self.add(format!(
445 "{}('{}','',#{magnitude},#{aspect})",
446 kind.step_entity,
447 step_text(id)
448 ))
449 } else {
450 let mut parts: Vec<String> = vec![base];
452 if let Some(system) = system {
453 parts.push(format!("GEOMETRIC_TOLERANCE_WITH_DATUM_REFERENCE((#{system}))"));
454 }
455 if let Some(condition) = condition {
456 parts.push(format!("GEOMETRIC_TOLERANCE_WITH_MODIFIERS(({condition}))"));
457 }
458 parts.push(format!("{}()", kind.step_entity));
459 parts.sort();
460 self.add(format!("({})", parts.join("")))
461 };
462 if annotation.flag("zoneDiameter") {
463 let form = self.add("TOLERANCE_ZONE_FORM('cylindrical or circular')");
464 self.add(format!(
465 "TOLERANCE_ZONE('','',#{},.F.,(#{tolerance}),#{form})",
466 self.context.product_shape
467 ));
468 }
469 Ok(Some(tolerance))
470 }
471 PmiGeometry::None
472 | PmiGeometry::Leader { .. }
473 | PmiGeometry::Note { .. }
474 | PmiGeometry::Explode { .. } => Ok(None),
475 }
476 }
477
478 fn write_datums(&mut self, pmi: &StepPmi<'_>) {
480 for view in &pmi.report.views {
481 for report in &view.annotations {
482 let PmiGeometry::Datum { letter, .. } = &report.geometry else {
483 continue;
484 };
485 if report.status != PmiStatus::Ok || !report.enabled || self.datums.contains_key(letter) {
486 continue;
487 }
488 let Some(name) = report.references.first() else {
489 continue;
490 };
491 let Some(item) = self.item(name) else {
492 continue;
493 };
494 let datum = self.add(format!(
495 "DATUM('{}','',#{},.F.,'{}')",
496 step_text(&report.id),
497 item.owner.product_shape,
498 step_text(letter)
499 ));
500 let feature = self.add(format!(
501 "DATUM_FEATURE('{}','',#{},.T.)",
502 step_text(name),
503 item.owner.product_shape
504 ));
505 self.link_aspect(feature, item, "datum feature");
506 self.add(format!("ID_ATTRIBUTE('{}',#{feature})", step_text(name)));
507 self.add(format!("SHAPE_ASPECT_RELATIONSHIP('','',#{feature},#{datum})"));
508 self.datums.insert(letter.clone(), datum);
509 self.aspects.entry(name.clone()).or_insert(feature);
510 }
511 }
512 }
513
514 fn callout(
516 &mut self,
517 report: &crate::feature_pipeline::pmi::PmiAnnotationReport,
518 style: &LayoutStyle,
519 ) -> Result<CalloutOut, String> {
520 let drawn = present(&report.geometry, report.label_world, &report.text, style);
521 let name = step_text(&report.id);
522 let set_name = step_text(&curve_set_name(report));
523 let polyline = |emitter: &mut Self, points: &[[f64; 3]], stats: &mut PolylineStats| -> Result<Option<usize>, String> {
524 if points.len() < 2 {
525 return Ok(None);
526 }
527 stats.add(points);
528 let mut ids = Vec::with_capacity(points.len());
529 for point in points {
530 ids.push(write_point(emitter.writer, Vec3::new(point[0], point[1], point[2]))?);
531 }
532 Ok(Some(emitter.add(format!("POLYLINE('{name}',{})", id_list(&ids)))))
533 };
534 let mut geometry_stats = PolylineStats::default();
537 let mut curves = Vec::new();
538 for line in &drawn.polylines {
539 if let Some(id) = polyline(self, line, &mut geometry_stats)? {
540 curves.push(id);
541 }
542 }
543 for arrow in &drawn.arrows {
544 let closed = [arrow[0], arrow[1], arrow[2], arrow[0]];
545 if let Some(id) = polyline(self, &closed, &mut geometry_stats)? {
546 curves.push(id);
547 }
548 }
549 for frame in &drawn.frames {
550 if let Some(id) = polyline(self, frame, &mut geometry_stats)? {
551 curves.push(id);
552 }
553 }
554 let mut text_stats = PolylineStats::default();
558 let mut text_curves = Vec::new();
559 for run in &drawn.texts {
560 for stroke in run.strokes() {
561 if let Some(id) = polyline(self, &stroke, &mut text_stats)? {
562 text_curves.push(id);
563 }
564 }
565 }
566 let mut contents = Vec::new();
567 let subset = |emitter: &mut Self, curves: &[usize]| -> Option<usize> {
568 if curves.is_empty() {
569 return None;
570 }
571 let set = emitter.add(format!("GEOMETRIC_CURVE_SET('{set_name}',{})", id_list(curves)));
572 Some(emitter.add(format!(
573 "ANNOTATION_CURVE_OCCURRENCE('{name}',(#{}),#{set})",
574 emitter.curve_style
575 )))
576 };
577 if let Some(id) = subset(self, &curves) {
578 contents.push(id);
579 }
580 let text_subset = subset(self, &text_curves);
581 contents.extend(text_subset);
582 let id = self.add(format!("DRAUGHTING_CALLOUT('{name}',{})", id_list(&contents)));
583 let text = drawn
584 .texts
585 .iter()
586 .map(|run| run.text.as_str())
587 .collect::<Vec<_>>()
588 .join(" ");
589 let mut total = geometry_stats;
590 total.merge(&text_stats);
591 Ok(CalloutOut {
592 id,
593 text,
594 total,
595 text_subset,
596 text_stats,
597 })
598 }
599
600 fn validation(&mut self, callout: &CalloutOut, global: usize) -> Result<(), String> {
606 let unicode = step_text(&callout.text);
607 let length_unit = self.context.length_unit;
608 let geometry_context = self.context.geometry_context;
609 let property = |emitter: &mut Self, item: usize, stats: &PolylineStats, with_length: bool| -> Result<(), String> {
610 let within = emitter.add(format!(
611 "CHARACTERIZED_ITEM_WITHIN_REPRESENTATION('','',#{item},#{global})"
612 ));
613 let definition = emitter.add(format!("PROPERTY_DEFINITION('pmi validation property','',#{within})"));
614 let mut items = Vec::new();
615 if with_length {
616 items.push(emitter.add(format!(
617 "MEASURE_REPRESENTATION_ITEM('polyline curve length',POSITIVE_LENGTH_MEASURE({}),#{length_unit})",
618 real(stats.length)?
619 )));
620 }
621 let centre = stats.centroid();
622 items.push(emitter.add(format!(
623 "CARTESIAN_POINT('polyline centre point',({},{},{}))",
624 real(centre[0])?,
625 real(centre[1])?,
626 real(centre[2])?
627 )));
628 items.push(emitter.add(format!(
629 "DESCRIPTIVE_REPRESENTATION_ITEM('equivalent unicode string','{unicode}')"
630 )));
631 let representation = emitter.add(format!(
632 "REPRESENTATION('',{},#{geometry_context})",
633 id_list(&items)
634 ));
635 emitter.add(format!("PROPERTY_DEFINITION_REPRESENTATION(#{definition},#{representation})"));
636 Ok(())
637 };
638 property(self, callout.id, &callout.total, true)?;
639 if let Some(text_subset) = callout.text_subset {
640 property(self, text_subset, &callout.text_stats, false)?;
641 }
642 Ok(())
643 }
644}
645
646struct CalloutOut {
648 id: usize,
650 text: String,
652 total: PolylineStats,
653 text_subset: Option<usize>,
655 text_stats: PolylineStats,
656}
657
658#[derive(Debug, Clone, Copy, Default)]
661struct PolylineStats {
662 length: f64,
663 moment: [f64; 3],
664 first: Option<[f64; 3]>,
666}
667
668impl PolylineStats {
669 fn add(&mut self, points: &[[f64; 3]]) {
670 if self.first.is_none() {
671 self.first = points.first().copied();
672 }
673 for pair in points.windows(2) {
674 let (a, b) = (pair[0], pair[1]);
675 let length = ((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2) + (b[2] - a[2]).powi(2)).sqrt();
676 self.length += length;
677 for axis in 0..3 {
678 self.moment[axis] += (a[axis] + b[axis]) * 0.5 * length;
679 }
680 }
681 }
682
683 fn merge(&mut self, other: &PolylineStats) {
684 self.length += other.length;
685 for axis in 0..3 {
686 self.moment[axis] += other.moment[axis];
687 }
688 if self.first.is_none() {
689 self.first = other.first;
690 }
691 }
692
693 fn centroid(&self) -> [f64; 3] {
694 if self.length > 1e-12 {
695 [self.moment[0] / self.length, self.moment[1] / self.length, self.moment[2] / self.length]
696 } else {
697 self.first.unwrap_or([0.0; 3])
698 }
699 }
700}
701
702fn curve_set_name(report: &crate::feature_pipeline::pmi::PmiAnnotationReport) -> String {
705 match report.kind.as_str() {
706 "linear" => "linear dimension".into(),
707 "radial" => match &report.geometry {
708 PmiGeometry::Radial { diameter: false, .. } => "radial dimension".into(),
709 _ => "diameter dimension".into(),
710 },
711 "angle" => "angular dimension".into(),
712 "holeCallout" => "diameter dimension".into(),
713 "datum" => "datum".into(),
714 "fcf" => match &report.geometry {
715 PmiGeometry::Fcf { frame, .. } => match frame.characteristic.as_str() {
716 "profileLine" => "profile of line".into(),
717 "profileSurface" => "profile of surface".into(),
718 "circularRunout" => "circular runout".into(),
719 "totalRunout" => "total runout".into(),
720 other => other.to_ascii_lowercase(),
721 },
722 _ => "general tolerance".into(),
723 },
724 _ => "note".into(),
725 }
726}
727
728fn material_condition(modifier: &str) -> Option<&'static str> {
730 match modifier.trim().to_ascii_uppercase().as_str() {
731 "MMC" => Some(".MAXIMUM_MATERIAL_REQUIREMENT."),
732 "LMC" => Some(".LEAST_MATERIAL_REQUIREMENT."),
733 _ => None,
734 }
735}
736
737fn write_camera(
739 emitter: &mut Emitter<'_, '_>,
740 name: &str,
741 camera: &crate::feature_pipeline::pmi::PmiCamera,
742) -> Result<usize, String> {
743 let eye = Vec3::new(camera.eye[0], camera.eye[1], camera.eye[2]);
744 let target = Vec3::new(camera.target[0], camera.target[1], camera.target[2]);
745 let view = camera.view_direction();
746 let view = Vec3::new(view[0], view[1], view[2]);
747 let up_hint = Vec3::new(camera.up[0], camera.up[1], camera.up[2]);
748 let up = crate::feature_pipeline::pmi::resolve::perpendicular_in_plane(view, up_hint);
749 let right = up.cross(view).normalized().unwrap_or(Vec3::new(1.0, 0.0, 0.0));
751 let distance = target.sub(eye).length().max(1e-6);
752 let aspect = if camera.viewport[1] > 0.0 {
753 camera.viewport[0] / camera.viewport[1]
754 } else {
755 1.5
756 };
757 let (projection, height) = match camera.projection {
758 PmiProjection::Orthographic { half_height } => (".PARALLEL.", 2.0 * half_height),
759 PmiProjection::Perspective { fov_y_deg } => {
760 (".CENTRAL.", 2.0 * distance * (fov_y_deg.to_radians() * 0.5).tan())
761 }
762 };
763 let width = height * aspect;
764 let corner = emitter.add(format!(
765 "CARTESIAN_POINT('',({},{}))",
766 real(-width * 0.5)?,
767 real(-height * 0.5)?
768 ));
769 let window_placement = emitter.add(format!("AXIS2_PLACEMENT_2D('',#{corner},$)"));
770 let window = emitter.add(format!(
771 "PLANAR_BOX('',{},{},#{window_placement})",
772 real(width)?,
773 real(height)?
774 ));
775 let projection_point = write_point(emitter.writer, Vec3::new(0.0, 0.0, 0.0))?;
776 let volume = emitter.add(format!(
777 "VIEW_VOLUME({projection},#{projection_point},{},{},.F.,{},.F.,.T.,#{window})",
778 real(distance)?,
779 real(0.0)?,
780 real(distance * 2.0)?
781 ));
782 let reference = write_placement(emitter.writer, eye, view, right)?;
783 Ok(emitter.add(format!(
784 "CAMERA_MODEL_D3('{}',#{reference},#{volume})",
785 step_text(name)
786 )))
787}
788
789pub(crate) fn write_pmi(
793 writer: &mut StepWriter,
794 context: &StepContext<'_>,
795 pmi: &StepPmi<'_>,
796) -> Result<usize, String> {
797 if pmi.state.views.is_empty() {
798 return Ok(0);
799 }
800 let env = Env::build("", &serde_json::Value::Null).unwrap_or_else(Env::poisoned);
801 let null_style = writer.add("PRESENTATION_STYLE_ASSIGNMENT((NULL_STYLE(.NULL.)))");
802 let colour = writer.add("COLOUR_RGB('',0.,0.,0.)");
803 let curve_font = writer.add("DRAUGHTING_PRE_DEFINED_CURVE_FONT('continuous')");
804 let curve_style = writer.add(format!(
805 "CURVE_STYLE('',#{curve_font},POSITIVE_LENGTH_MEASURE({}),#{colour})",
806 real(LINE_WIDTH)?
807 ));
808 let curve_style = writer.add(format!("PRESENTATION_STYLE_ASSIGNMENT((#{curve_style}))"));
809 let mut emitter = Emitter {
810 writer,
811 context,
812 aspects: HashMap::default(),
813 datums: HashMap::default(),
814 unresolved: std::collections::BTreeSet::new(),
815 null_style,
816 curve_style,
817 };
818 let identity = write_placement(
820 emitter.writer,
821 Vec3::new(0.0, 0.0, 0.0),
822 Vec3::new(0.0, 0.0, 1.0),
823 Vec3::new(1.0, 0.0, 0.0),
824 )?;
825 let map = emitter.add(format!("REPRESENTATION_MAP(#{identity},#{})", context.representation));
826 let mapped = emitter.add(format!("MAPPED_ITEM('',#{map},#{identity})"));
827 let styled_shape = emitter.add(format!("STYLED_ITEM('',(#{null_style}),#{mapped})"));
828
829 emitter.write_datums(pmi);
830
831 struct ViewOut {
833 name: String,
834 planes: Vec<usize>,
836 camera: Option<usize>,
837 callouts: Vec<CalloutOut>,
838 links: Vec<(usize, usize)>,
839 }
840 let mut views_out: Vec<ViewOut> = Vec::new();
841 for view in &pmi.state.views {
842 let Some(view_report) = pmi.report.view(&view.id) else {
843 continue;
844 };
845 let camera = view.camera.as_ref();
846 let (view_dir, view_up, target) = match camera {
847 Some(camera) => (camera.view_direction(), camera.up, camera.target),
848 None => ([0.0, 0.0, -1.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0]),
849 };
850 let text_height = view.display.text_size_pt * MM_PER_POINT;
851 let style = LayoutStyle {
852 arrow: text_height * 0.9,
853 text_height,
854 view_dir,
855 view_up,
856 plane: None,
857 };
858 let view_vec = Vec3::new(view_dir[0], view_dir[1], view_dir[2]);
859 let up_vec = crate::feature_pipeline::pmi::resolve::perpendicular_in_plane(
860 view_vec,
861 Vec3::new(view_up[0], view_up[1], view_up[2]),
862 );
863 let right = up_vec.cross(view_vec.scale(-1.0)).normalized().unwrap_or(Vec3::new(1.0, 0.0, 0.0));
864 let mut callouts = Vec::new();
868 let mut view_aligned = Vec::new();
869 let mut plane_groups: Vec<(String, crate::feature_pipeline::pmi::PmiPlane, Vec<usize>)> = Vec::new();
870 let mut links = Vec::new();
871 for (annotation, report) in view.annotations.iter().zip(view_report.annotations.iter()) {
872 if report.status != PmiStatus::Ok || !report.enabled {
873 continue;
874 }
875 if matches!(report.geometry, PmiGeometry::Explode { .. } | PmiGeometry::None) {
876 continue;
877 }
878 let semantic = emitter.semantic(annotation, report, &env)?;
879 let row_style = LayoutStyle {
883 plane: report.plane.or(Some(crate::feature_pipeline::pmi::PmiPlane {
884 origin: report.label_world,
885 normal: [-view_dir[0], -view_dir[1], -view_dir[2]],
886 x_axis: [right.x, right.y, right.z],
887 })),
888 ..style
889 };
890 let callout = emitter.callout(report, &row_style)?;
891 let callout_id = callout.id;
892 callouts.push(callout);
893 match report.plane {
894 Some(plane) => {
895 let key = annotation.plane_ref().unwrap_or("").to_string();
896 match plane_groups.iter_mut().find(|(name, _, _)| *name == key) {
897 Some(group) => group.2.push(callout_id),
898 None => plane_groups.push((key, plane, vec![callout_id])),
899 }
900 }
901 None => view_aligned.push(callout_id),
902 }
903 if let Some(semantic) = semantic {
904 links.push((semantic, callout_id));
905 }
906 }
907 let placement = write_placement(
908 emitter.writer,
909 Vec3::new(target[0], target[1], target[2]),
910 view_vec.scale(-1.0),
911 right,
912 )?;
913 let plane_geometry = emitter.add(format!("PLANE('',#{placement})"));
914 let mut planes = vec![emitter.add(format!(
915 "ANNOTATION_PLANE('{}',(#{null_style}),#{plane_geometry},{})",
916 step_text(&view.name),
917 id_list(&view_aligned)
918 ))];
919 for (key, plane, members) in &plane_groups {
920 let placement = write_placement(
921 emitter.writer,
922 Vec3::new(plane.origin[0], plane.origin[1], plane.origin[2]),
923 Vec3::new(plane.normal[0], plane.normal[1], plane.normal[2]),
924 Vec3::new(plane.x_axis[0], plane.x_axis[1], plane.x_axis[2]),
925 )?;
926 let geometry = emitter.add(format!("PLANE('',#{placement})"));
927 planes.push(emitter.add(format!(
928 "ANNOTATION_PLANE('{}',(#{null_style}),#{geometry},{})",
929 step_text(&format!("{} / {key}", view.name)),
930 id_list(members)
931 )));
932 }
933 let camera_id = match camera {
934 Some(camera) => Some(write_camera(&mut emitter, &view.name, camera)?),
935 None => None,
936 };
937 views_out.push(ViewOut {
938 name: view.name.clone(),
939 planes,
940 camera: camera_id,
941 callouts,
942 links,
943 });
944 }
945
946 let mut global_items = vec![styled_shape];
948 global_items.extend(views_out.iter().flat_map(|view| view.planes.iter().copied()));
949 let global = emitter.add(format!(
950 "DRAUGHTING_MODEL('',{},#{})",
951 id_list(&global_items),
952 context.geometry_context
953 ));
954 for view in &views_out {
955 let mut items = vec![styled_shape];
956 if let Some(camera) = view.camera {
957 items.push(camera);
958 }
959 items.extend(view.callouts.iter().map(|callout| callout.id));
960 let model = emitter.add(format!(
961 "DRAUGHTING_MODEL('{}',{},#{})",
962 step_text(&view.name),
963 id_list(&items),
964 context.geometry_context
965 ));
966 emitter.add(format!(
967 "MECHANICAL_DESIGN_AND_DRAUGHTING_RELATIONSHIP('','',#{model},#{global})"
968 ));
969 for (semantic, callout) in &view.links {
970 emitter.add(format!(
971 "DRAUGHTING_MODEL_ITEM_ASSOCIATION('PMI representation to presentation link','',#{semantic},#{global},#{callout})"
972 ));
973 }
974 }
975 for view in &views_out {
977 for callout in &view.callouts {
978 emitter.validation(callout, global)?;
979 }
980 }
981 Ok(emitter.unresolved.len())
982}