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