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