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 { .. } => Ok(None),
476 }
477 }
478
479 fn write_datums(&mut self, pmi: &StepPmi<'_>) {
481 for view in &pmi.report.views {
482 for report in &view.annotations {
483 let PmiGeometry::Datum { letter, .. } = &report.geometry else {
484 continue;
485 };
486 if report.status != PmiStatus::Ok || !report.enabled || self.datums.contains_key(letter) {
487 continue;
488 }
489 let Some(name) = report.references.first() else {
490 continue;
491 };
492 let Some(item) = self.item(name) else {
493 continue;
494 };
495 let datum = self.add(format!(
496 "DATUM('{}','',#{},.F.,'{}')",
497 step_text(&report.id),
498 item.owner.product_shape,
499 step_text(letter)
500 ));
501 let feature = self.add(format!(
502 "DATUM_FEATURE('{}','',#{},.T.)",
503 step_text(name),
504 item.owner.product_shape
505 ));
506 self.link_aspect(feature, item, "datum feature");
507 self.add(format!("ID_ATTRIBUTE('{}',#{feature})", step_text(name)));
508 self.add(format!("SHAPE_ASPECT_RELATIONSHIP('','',#{feature},#{datum})"));
509 self.datums.insert(letter.clone(), datum);
510 self.aspects.entry(name.clone()).or_insert(feature);
511 }
512 }
513 }
514
515 fn callout(
517 &mut self,
518 report: &crate::feature_pipeline::pmi::PmiAnnotationReport,
519 style: &LayoutStyle,
520 ) -> Result<CalloutOut, String> {
521 let drawn = present(&report.geometry, report.label_world, &report.text, style);
522 let name = step_text(&report.id);
523 let set_name = step_text(&curve_set_name(report));
524 let polyline = |emitter: &mut Self, points: &[[f64; 3]], stats: &mut PolylineStats| -> Result<Option<usize>, String> {
525 if points.len() < 2 {
526 return Ok(None);
527 }
528 stats.add(points);
529 let mut ids = Vec::with_capacity(points.len());
530 for point in points {
531 ids.push(write_point(emitter.writer, Vec3::new(point[0], point[1], point[2]))?);
532 }
533 Ok(Some(emitter.add(format!("POLYLINE('{name}',{})", id_list(&ids)))))
534 };
535 let mut geometry_stats = PolylineStats::default();
538 let mut curves = Vec::new();
539 for line in &drawn.polylines {
540 if let Some(id) = polyline(self, line, &mut geometry_stats)? {
541 curves.push(id);
542 }
543 }
544 for arrow in &drawn.arrows {
545 let closed = [arrow[0], arrow[1], arrow[2], arrow[0]];
546 if let Some(id) = polyline(self, &closed, &mut geometry_stats)? {
547 curves.push(id);
548 }
549 }
550 for frame in &drawn.frames {
551 if let Some(id) = polyline(self, frame, &mut geometry_stats)? {
552 curves.push(id);
553 }
554 }
555 let mut text_stats = PolylineStats::default();
559 let mut text_curves = Vec::new();
560 for run in &drawn.texts {
561 for stroke in run.strokes() {
562 if let Some(id) = polyline(self, &stroke, &mut text_stats)? {
563 text_curves.push(id);
564 }
565 }
566 }
567 let mut contents = Vec::new();
568 let subset = |emitter: &mut Self, curves: &[usize]| -> Option<usize> {
569 if curves.is_empty() {
570 return None;
571 }
572 let set = emitter.add(format!("GEOMETRIC_CURVE_SET('{set_name}',{})", id_list(curves)));
573 Some(emitter.add(format!(
574 "ANNOTATION_CURVE_OCCURRENCE('{name}',(#{}),#{set})",
575 emitter.curve_style
576 )))
577 };
578 if let Some(id) = subset(self, &curves) {
579 contents.push(id);
580 }
581 let text_subset = subset(self, &text_curves);
582 contents.extend(text_subset);
583 let id = self.add(format!("DRAUGHTING_CALLOUT('{name}',{})", id_list(&contents)));
584 let text = drawn
585 .texts
586 .iter()
587 .map(|run| run.text.as_str())
588 .collect::<Vec<_>>()
589 .join(" ");
590 let mut total = geometry_stats;
591 total.merge(&text_stats);
592 Ok(CalloutOut {
593 id,
594 text,
595 total,
596 text_subset,
597 text_stats,
598 })
599 }
600
601 fn validation(&mut self, callout: &CalloutOut, global: usize) -> Result<(), String> {
607 let unicode = step_text(&callout.text);
608 let length_unit = self.context.length_unit;
609 let geometry_context = self.context.geometry_context;
610 let property = |emitter: &mut Self, item: usize, stats: &PolylineStats, with_length: bool| -> Result<(), String> {
611 let within = emitter.add(format!(
612 "CHARACTERIZED_ITEM_WITHIN_REPRESENTATION('','',#{item},#{global})"
613 ));
614 let definition = emitter.add(format!("PROPERTY_DEFINITION('pmi validation property','',#{within})"));
615 let mut items = Vec::new();
616 if with_length {
617 items.push(emitter.add(format!(
618 "MEASURE_REPRESENTATION_ITEM('polyline curve length',POSITIVE_LENGTH_MEASURE({}),#{length_unit})",
619 real(stats.length)?
620 )));
621 }
622 let centre = stats.centroid();
623 items.push(emitter.add(format!(
624 "CARTESIAN_POINT('polyline centre point',({},{},{}))",
625 real(centre[0])?,
626 real(centre[1])?,
627 real(centre[2])?
628 )));
629 items.push(emitter.add(format!(
630 "DESCRIPTIVE_REPRESENTATION_ITEM('equivalent unicode string','{unicode}')"
631 )));
632 let representation = emitter.add(format!(
633 "REPRESENTATION('',{},#{geometry_context})",
634 id_list(&items)
635 ));
636 emitter.add(format!("PROPERTY_DEFINITION_REPRESENTATION(#{definition},#{representation})"));
637 Ok(())
638 };
639 property(self, callout.id, &callout.total, true)?;
640 if let Some(text_subset) = callout.text_subset {
641 property(self, text_subset, &callout.text_stats, false)?;
642 }
643 Ok(())
644 }
645}
646
647struct CalloutOut {
649 id: usize,
651 text: String,
653 total: PolylineStats,
654 text_subset: Option<usize>,
656 text_stats: PolylineStats,
657}
658
659#[derive(Debug, Clone, Copy, Default)]
662struct PolylineStats {
663 length: f64,
664 moment: [f64; 3],
665 first: Option<[f64; 3]>,
667}
668
669impl PolylineStats {
670 fn add(&mut self, points: &[[f64; 3]]) {
671 if self.first.is_none() {
672 self.first = points.first().copied();
673 }
674 for pair in points.windows(2) {
675 let (a, b) = (pair[0], pair[1]);
676 let length = ((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2) + (b[2] - a[2]).powi(2)).sqrt();
677 self.length += length;
678 for axis in 0..3 {
679 self.moment[axis] += (a[axis] + b[axis]) * 0.5 * length;
680 }
681 }
682 }
683
684 fn merge(&mut self, other: &PolylineStats) {
685 self.length += other.length;
686 for axis in 0..3 {
687 self.moment[axis] += other.moment[axis];
688 }
689 if self.first.is_none() {
690 self.first = other.first;
691 }
692 }
693
694 fn centroid(&self) -> [f64; 3] {
695 if self.length > 1e-12 {
696 [self.moment[0] / self.length, self.moment[1] / self.length, self.moment[2] / self.length]
697 } else {
698 self.first.unwrap_or([0.0; 3])
699 }
700 }
701}
702
703fn curve_set_name(report: &crate::feature_pipeline::pmi::PmiAnnotationReport) -> String {
706 match report.kind.as_str() {
707 "linear" => "linear dimension".into(),
708 "radial" => match &report.geometry {
709 PmiGeometry::Radial { diameter: false, .. } => "radial dimension".into(),
710 _ => "diameter dimension".into(),
711 },
712 "angle" => "angular dimension".into(),
713 "holeCallout" => "diameter dimension".into(),
714 "datum" => "datum".into(),
715 "fcf" => match &report.geometry {
716 PmiGeometry::Fcf { frame, .. } => match frame.characteristic.as_str() {
717 "profileLine" => "profile of line".into(),
718 "profileSurface" => "profile of surface".into(),
719 "circularRunout" => "circular runout".into(),
720 "totalRunout" => "total runout".into(),
721 other => other.to_ascii_lowercase(),
722 },
723 _ => "general tolerance".into(),
724 },
725 _ => "note".into(),
726 }
727}
728
729fn material_condition(modifier: &str) -> Option<&'static str> {
731 match modifier.trim().to_ascii_uppercase().as_str() {
732 "MMC" => Some(".MAXIMUM_MATERIAL_REQUIREMENT."),
733 "LMC" => Some(".LEAST_MATERIAL_REQUIREMENT."),
734 _ => None,
735 }
736}
737
738fn write_camera(
740 emitter: &mut Emitter<'_, '_>,
741 name: &str,
742 camera: &crate::feature_pipeline::pmi::PmiCamera,
743) -> Result<usize, String> {
744 let eye = Vec3::new(camera.eye[0], camera.eye[1], camera.eye[2]);
745 let target = Vec3::new(camera.target[0], camera.target[1], camera.target[2]);
746 let view = camera.view_direction();
747 let view = Vec3::new(view[0], view[1], view[2]);
748 let up_hint = Vec3::new(camera.up[0], camera.up[1], camera.up[2]);
749 let up = crate::feature_pipeline::pmi::resolve::perpendicular_in_plane(view, up_hint);
750 let right = up.cross(view).normalized().unwrap_or(Vec3::new(1.0, 0.0, 0.0));
752 let distance = target.sub(eye).length().max(1e-6);
753 let aspect = if camera.viewport[1] > 0.0 {
754 camera.viewport[0] / camera.viewport[1]
755 } else {
756 1.5
757 };
758 let (projection, height) = match camera.projection {
759 PmiProjection::Orthographic { half_height } => (".PARALLEL.", 2.0 * half_height),
760 PmiProjection::Perspective { fov_y_deg } => {
761 (".CENTRAL.", 2.0 * distance * (fov_y_deg.to_radians() * 0.5).tan())
762 }
763 };
764 let width = height * aspect;
765 let corner = emitter.add(format!(
766 "CARTESIAN_POINT('',({},{}))",
767 real(-width * 0.5)?,
768 real(-height * 0.5)?
769 ));
770 let window_placement = emitter.add(format!("AXIS2_PLACEMENT_2D('',#{corner},$)"));
771 let window = emitter.add(format!(
772 "PLANAR_BOX('',{},{},#{window_placement})",
773 real(width)?,
774 real(height)?
775 ));
776 let projection_point = write_point(emitter.writer, Vec3::new(0.0, 0.0, 0.0))?;
777 let volume = emitter.add(format!(
778 "VIEW_VOLUME({projection},#{projection_point},{},{},.F.,{},.F.,.T.,#{window})",
779 real(distance)?,
780 real(0.0)?,
781 real(distance * 2.0)?
782 ));
783 let reference = write_placement(emitter.writer, eye, view, right)?;
784 Ok(emitter.add(format!(
785 "CAMERA_MODEL_D3('{}',#{reference},#{volume})",
786 step_text(name)
787 )))
788}
789
790pub(crate) fn write_pmi(
794 writer: &mut StepWriter,
795 context: &StepContext<'_>,
796 pmi: &StepPmi<'_>,
797) -> Result<usize, String> {
798 if pmi.state.views.is_empty() {
799 return Ok(0);
800 }
801 let env = Env::build("", &serde_json::Value::Null).unwrap_or_else(Env::poisoned);
802 let null_style = write_style_assignment(writer, "NULL_STYLE(.NULL.)");
806 let colour = write_colour_rgb(writer, [0.0, 0.0, 0.0])?;
807 let curve_font = writer.add("DRAUGHTING_PRE_DEFINED_CURVE_FONT('continuous')");
808 let curve_style = writer.add(format!(
809 "CURVE_STYLE('',#{curve_font},POSITIVE_LENGTH_MEASURE({}),#{colour})",
810 real(LINE_WIDTH)?
811 ));
812 let curve_style = write_style_assignment(writer, &format!("#{curve_style}"));
813 let mut emitter = Emitter {
814 writer,
815 context,
816 aspects: HashMap::default(),
817 datums: HashMap::default(),
818 unresolved: std::collections::BTreeSet::new(),
819 null_style,
820 curve_style,
821 };
822 let identity = write_placement(
824 emitter.writer,
825 Vec3::new(0.0, 0.0, 0.0),
826 Vec3::new(0.0, 0.0, 1.0),
827 Vec3::new(1.0, 0.0, 0.0),
828 )?;
829 let map = emitter.add(format!("REPRESENTATION_MAP(#{identity},#{})", context.representation));
830 let mapped = emitter.add(format!("MAPPED_ITEM('',#{map},#{identity})"));
831 let styled_shape = write_styled_item(emitter.writer, "", null_style, mapped);
832
833 emitter.write_datums(pmi);
834
835 struct ViewOut {
837 name: String,
838 planes: Vec<usize>,
840 camera: Option<usize>,
841 callouts: Vec<CalloutOut>,
842 links: Vec<(usize, usize)>,
843 }
844 let mut views_out: Vec<ViewOut> = Vec::new();
845 for view in &pmi.state.views {
846 let Some(view_report) = pmi.report.view(&view.id) else {
847 continue;
848 };
849 let camera = view.camera.as_ref();
850 let (view_dir, view_up, target) = match camera {
851 Some(camera) => (camera.view_direction(), camera.up, camera.target),
852 None => ([0.0, 0.0, -1.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0]),
853 };
854 let text_height = view.display.text_size_pt * MM_PER_POINT;
855 let style = LayoutStyle {
856 arrow: text_height * 0.9,
857 text_height,
858 view_dir,
859 view_up,
860 plane: None,
861 };
862 let view_vec = Vec3::new(view_dir[0], view_dir[1], view_dir[2]);
863 let up_vec = crate::feature_pipeline::pmi::resolve::perpendicular_in_plane(
864 view_vec,
865 Vec3::new(view_up[0], view_up[1], view_up[2]),
866 );
867 let right = up_vec.cross(view_vec.scale(-1.0)).normalized().unwrap_or(Vec3::new(1.0, 0.0, 0.0));
868 let mut callouts = Vec::new();
872 let mut view_aligned = Vec::new();
873 let mut plane_groups: Vec<(String, crate::feature_pipeline::pmi::PmiPlane, Vec<usize>)> = Vec::new();
874 let mut links = Vec::new();
875 for (annotation, report) in view.annotations.iter().zip(view_report.annotations.iter()) {
876 if report.status != PmiStatus::Ok || !report.enabled {
877 continue;
878 }
879 if matches!(report.geometry, PmiGeometry::Explode { .. } | PmiGeometry::None) {
880 continue;
881 }
882 let semantic = emitter.semantic(annotation, report, &env)?;
883 let row_style = LayoutStyle {
887 plane: report.plane.or(Some(crate::feature_pipeline::pmi::PmiPlane {
888 origin: report.label_world,
889 normal: [-view_dir[0], -view_dir[1], -view_dir[2]],
890 x_axis: [right.x, right.y, right.z],
891 })),
892 ..style
893 };
894 let callout = emitter.callout(report, &row_style)?;
895 let callout_id = callout.id;
896 callouts.push(callout);
897 match report.plane {
898 Some(plane) => {
899 let key = annotation.plane_ref().unwrap_or("").to_string();
900 match plane_groups.iter_mut().find(|(name, _, _)| *name == key) {
901 Some(group) => group.2.push(callout_id),
902 None => plane_groups.push((key, plane, vec![callout_id])),
903 }
904 }
905 None => view_aligned.push(callout_id),
906 }
907 if let Some(semantic) = semantic {
908 links.push((semantic, callout_id));
909 }
910 }
911 let placement = write_placement(
912 emitter.writer,
913 Vec3::new(target[0], target[1], target[2]),
914 view_vec.scale(-1.0),
915 right,
916 )?;
917 let plane_geometry = emitter.add(format!("PLANE('',#{placement})"));
918 let mut planes = vec![emitter.add(format!(
919 "ANNOTATION_PLANE('{}',(#{null_style}),#{plane_geometry},{})",
920 step_text(&view.name),
921 id_list(&view_aligned)
922 ))];
923 for (key, plane, members) in &plane_groups {
924 let placement = write_placement(
925 emitter.writer,
926 Vec3::new(plane.origin[0], plane.origin[1], plane.origin[2]),
927 Vec3::new(plane.normal[0], plane.normal[1], plane.normal[2]),
928 Vec3::new(plane.x_axis[0], plane.x_axis[1], plane.x_axis[2]),
929 )?;
930 let geometry = emitter.add(format!("PLANE('',#{placement})"));
931 planes.push(emitter.add(format!(
932 "ANNOTATION_PLANE('{}',(#{null_style}),#{geometry},{})",
933 step_text(&format!("{} / {key}", view.name)),
934 id_list(members)
935 )));
936 }
937 let camera_id = match camera {
938 Some(camera) => Some(write_camera(&mut emitter, &view.name, camera)?),
939 None => None,
940 };
941 views_out.push(ViewOut {
942 name: view.name.clone(),
943 planes,
944 camera: camera_id,
945 callouts,
946 links,
947 });
948 }
949
950 let mut global_items = vec![styled_shape];
952 global_items.extend(views_out.iter().flat_map(|view| view.planes.iter().copied()));
953 let global = emitter.add(format!(
954 "DRAUGHTING_MODEL('',{},#{})",
955 id_list(&global_items),
956 context.geometry_context
957 ));
958 for view in &views_out {
959 let mut items = vec![styled_shape];
960 if let Some(camera) = view.camera {
961 items.push(camera);
962 }
963 items.extend(view.callouts.iter().map(|callout| callout.id));
964 let model = emitter.add(format!(
965 "DRAUGHTING_MODEL('{}',{},#{})",
966 step_text(&view.name),
967 id_list(&items),
968 context.geometry_context
969 ));
970 emitter.add(format!(
971 "MECHANICAL_DESIGN_AND_DRAUGHTING_RELATIONSHIP('','',#{model},#{global})"
972 ));
973 for (semantic, callout) in &view.links {
974 emitter.add(format!(
975 "DRAUGHTING_MODEL_ITEM_ASSOCIATION('PMI representation to presentation link','',#{semantic},#{global},#{callout})"
976 ));
977 }
978 }
979 for view in &views_out {
981 for callout in &view.callouts {
982 emitter.validation(callout, global)?;
983 }
984 }
985 Ok(emitter.unresolved.len())
986}