1use crate::scene::RenderScene;
42use brep_kernel::{
43 pmi_present, PmiAnnotationReport, PmiCamera, PmiLayoutStyle, PmiProjection, PmiStatus,
44 PmiView, PmiViewReport,
45};
46
47use super::{PlacedView, ORTHOGRAPHIC};
48
49pub const EDGE_ANGLE_DEG: f64 = 30.0;
53
54pub const MM_PER_POINT: f64 = 25.4 / 72.0;
57
58pub(super) const ARROW_PER_TEXT: f64 = 12.0 / 11.0;
62
63pub(super) const TEXT_ADVANCE: f64 = 0.75;
69
70const TEXT_PAD: f64 = 0.3;
73
74pub const HATCH_SPACING_MM: f64 = 2.5;
79
80const MAX_HATCH_LINES: usize = 2000;
86
87pub const EDGE_ON_COS: f64 = 1e-3;
93
94#[derive(Debug, Clone, PartialEq, serde::Serialize)]
101pub struct SheetDrawing {
102 pub id: String,
103 pub name: String,
104 #[serde(rename = "widthMm")]
105 pub width_mm: f64,
106 #[serde(rename = "heightMm")]
107 pub height_mm: f64,
108 pub views: Vec<ViewDrawing>,
109 pub dimensions: Vec<super::dimension::DimensionDrawing>,
114 pub ordinates: Vec<super::ordinate::OrdinateDrawing>,
118 #[serde(skip_serializing_if = "Option::is_none")]
124 pub frame: Option<super::frame::FrameDrawing>,
125}
126
127#[derive(Debug, Clone, PartialEq, serde::Serialize)]
131pub struct ViewDrawing {
132 pub id: String,
134 pub view: String,
136 pub name: String,
138 pub position: [f64; 2],
139 pub scale: f64,
140 #[serde(skip_serializing_if = "String::is_empty")]
143 pub error: String,
144 pub edges: Vec<Vec<[f64; 2]>>,
146 pub annotations: Vec<AnnotationDrawing>,
148 pub anchors: Vec<AnchorPoint>,
154 #[serde(rename = "textMm")]
158 pub text_mm: f64,
159 #[serde(skip_serializing_if = "Option::is_none")]
162 pub section: Option<SectionDrawing>,
163 #[serde(rename = "sectionMarks", skip_serializing_if = "Vec::is_empty")]
166 pub section_marks: Vec<SectionMark>,
167 #[serde(skip_serializing_if = "Option::is_none")]
170 pub detail: Option<DetailDrawing>,
171 #[serde(rename = "detailMarks", skip_serializing_if = "Vec::is_empty")]
174 pub detail_marks: Vec<DetailMark>,
175 pub bounds: [f64; 4],
178 #[serde(rename = "hiddenLine", skip_serializing_if = "String::is_empty")]
184 pub hidden_line: String,
185 #[serde(skip)]
191 pub tolerances: Vec<ToleranceSource>,
192}
193
194#[derive(Debug, Clone, PartialEq)]
197pub struct ToleranceSource {
198 pub id: String,
200 pub measures: Measures,
201 pub block: brep_kernel::ToleranceBlock,
202 pub reference: bool,
203}
204
205#[derive(Debug, Clone, PartialEq)]
207pub enum Measures {
208 Linear { a: [f64; 2], b: [f64; 2] },
212 Radial { centre: [f64; 2], radius: f64, diameter: bool },
215}
216
217impl ViewDrawing {
218 fn empty(placed: &PlacedView, name: String) -> ViewDrawing {
222 ViewDrawing {
223 id: placed.id.clone(),
224 view: placed.view.clone(),
225 name,
226 position: placed.position,
227 scale: placed.scale,
228 error: String::new(),
229 edges: Vec::new(),
230 annotations: Vec::new(),
231 anchors: Vec::new(),
232 text_mm: 0.0,
233 section: None,
234 section_marks: Vec::new(),
235 detail: None,
236 detail_marks: Vec::new(),
237 bounds: [placed.position[0], placed.position[1], placed.position[0], placed.position[1]],
238 hidden_line: String::new(),
239 tolerances: Vec::new(),
240 }
241 }
242}
243
244#[derive(Debug, Clone, PartialEq, serde::Serialize)]
250pub struct DetailDrawing {
251 pub label: String,
252 pub centre: [f64; 2],
254 #[serde(rename = "radiusMm")]
257 pub radius_mm: f64,
258 pub circle: Vec<[f64; 2]>,
260 pub texts: Vec<SheetText>,
262}
263
264#[derive(Debug, Clone, PartialEq, serde::Serialize)]
267pub struct DetailMark {
268 pub id: String,
270 pub label: String,
271 pub centre: [f64; 2],
272 #[serde(rename = "radiusMm")]
273 pub radius_mm: f64,
274 pub lines: Vec<Vec<[f64; 2]>>,
275 pub texts: Vec<SheetText>,
276}
277
278#[derive(Debug, Clone, PartialEq, serde::Serialize)]
286pub struct SectionDrawing {
287 pub label: String,
289 pub hatch: Vec<Vec<[f64; 2]>>,
292 pub cut: Vec<Vec<[f64; 2]>>,
299 pub texts: Vec<SheetText>,
301}
302
303#[derive(Debug, Clone, PartialEq, serde::Serialize)]
307pub struct SectionMark {
308 pub id: String,
310 pub label: String,
311 pub lines: Vec<Vec<[f64; 2]>>,
312 pub texts: Vec<SheetText>,
313}
314
315#[derive(Debug, Clone, PartialEq, serde::Serialize)]
317pub struct AnnotationDrawing {
318 pub id: String,
319 #[serde(rename = "type")]
320 pub kind: String,
321 pub text: String,
324 pub lines: Vec<Vec<[f64; 2]>>,
326 pub texts: Vec<SheetText>,
327 #[serde(skip_serializing_if = "String::is_empty")]
333 pub note: String,
334}
335
336#[derive(Debug, Clone, PartialEq, serde::Serialize)]
345pub struct AnchorPoint {
346 #[serde(rename = "ref")]
347 pub anchor: String,
348 pub kind: String,
350 pub at: [f64; 2],
352 pub model: [f64; 3],
356 #[serde(skip_serializing_if = "Option::is_none")]
367 pub dir: Option<[f64; 2]>,
368 #[serde(skip_serializing_if = "Option::is_none")]
370 pub radius: Option<f64>,
371 #[serde(skip_serializing_if = "Option::is_none")]
375 pub u: Option<[f64; 2]>,
376 #[serde(skip_serializing_if = "Option::is_none")]
377 pub v: Option<[f64; 2]>,
378}
379
380#[derive(Debug, Clone, PartialEq, serde::Serialize)]
398pub struct SheetText {
399 pub text: String,
400 pub anchor: [f64; 2],
401 pub height: f64,
402 pub angle: f64,
403 pub right: [f64; 2],
404 pub up: [f64; 2],
405}
406
407impl SheetText {
408 pub fn new(text: String, anchor: [f64; 2], right: [f64; 2], up: [f64; 2]) -> SheetText {
411 SheetText {
412 text,
413 anchor,
414 height: (up[0] * up[0] + up[1] * up[1]).sqrt(),
415 angle: right[1].atan2(right[0]).to_degrees(),
418 right,
419 up,
420 }
421 }
422
423 pub fn flat(text: String, anchor: [f64; 2], height: f64) -> SheetText {
427 SheetText::new(text, anchor, [height, 0.0], [0.0, -height])
428 }
429}
430
431#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
439pub struct ViewFrame {
440 pub target: [f64; 3],
441 pub right: [f64; 3],
442 pub up: [f64; 3],
443 pub forward: [f64; 3],
444}
445
446fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
447 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
448}
449
450fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
451 [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
452}
453
454fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
455 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
456}
457
458fn norm(v: [f64; 3]) -> [f64; 3] {
459 let len = dot(v, v).sqrt();
460 if len < 1e-12 {
461 [0.0, 0.0, 0.0]
462 } else {
463 [v[0] / len, v[1] / len, v[2] / len]
464 }
465}
466
467impl ViewFrame {
468 pub fn of(camera: &PmiCamera) -> ViewFrame {
472 let forward = norm(sub(camera.target, camera.eye));
473 let forward = if forward == [0.0, 0.0, 0.0] { [0.0, 0.0, -1.0] } else { forward };
474 let mut right = norm(cross(forward, camera.up));
475 if right == [0.0, 0.0, 0.0] {
476 right = norm(cross(forward, [0.0, 0.0, 1.0]));
479 if right == [0.0, 0.0, 0.0] {
480 right = norm(cross(forward, [0.0, 1.0, 0.0]));
481 }
482 }
483 let up = cross(right, forward);
484 ViewFrame { target: camera.target, right, up, forward }
485 }
486
487 pub fn to_view(&self, p: [f64; 3]) -> [f64; 3] {
490 let d = sub(p, self.target);
491 [dot(d, self.right), dot(d, self.up), dot(d, self.forward)]
492 }
493}
494
495fn to_paper(placed: &PlacedView, x: f64, y: f64) -> [f64; 2] {
497 [placed.position[0] + x * placed.scale, placed.position[1] - y * placed.scale]
498}
499
500pub fn project_view(
508 scene: &RenderScene,
509 placed: &PlacedView,
510 view: Option<&PmiView>,
511 report: Option<&PmiViewReport>,
512) -> ViewDrawing {
513 let (unposed, env) = (Unposed::new(), brep_kernel::Env::default());
514 project_view_through(&Model { scene, unposed: &unposed, env: &env }, placed, view, report, None, &mut LinesMode::Compute)
515}
516
517pub struct Model<'a> {
523 pub scene: &'a RenderScene,
524 pub unposed: &'a Unposed,
525 pub env: &'a brep_kernel::Env,
526}
527
528pub type Unposed = std::collections::HashMap<String, crate::scene::SolidDisplay>;
532
533#[derive(Debug, Clone, Copy, PartialEq, Eq)]
535pub enum HiddenLine {
536 Analytic,
538 Mesh,
541}
542
543impl HiddenLine {
544 pub fn label(self) -> &'static str {
545 match self {
546 HiddenLine::Analytic => "analytic",
547 HiddenLine::Mesh => "mesh",
548 }
549 }
550}
551
552pub struct SectionSetup {
556 pub frame: ViewFrame,
557 pub plane: ClipPlane,
558 pub label: String,
559}
560
561pub struct DetailSetup {
566 pub frame: ViewFrame,
567 pub radius: f64,
568 pub label: String,
569}
570
571pub enum Derived {
574 Section(SectionSetup),
575 Detail(DetailSetup),
576}
577
578pub fn project_view_through(
596 model: &Model,
597 placed: &PlacedView,
598 view: Option<&PmiView>,
599 report: Option<&PmiViewReport>,
600 derived: Option<Derived>,
601 mode: &mut LinesMode,
602) -> ViewDrawing {
603 let (scene, unposed) = (model.scene, model.unposed);
604 let mut out = ViewDrawing::empty(placed, view.map(|v| v.name.clone()).unwrap_or_default());
605 let Some(view) = view else {
606 out.error = format!("no PMI view '{}'", placed.view);
607 return out;
608 };
609 let Some(camera) = view.camera.as_ref() else {
610 out.error = format!("PMI view '{}' has no captured camera", placed.view);
611 return out;
612 };
613 if !matches!(camera.projection, PmiProjection::Orthographic { .. }) {
614 out.error = format!(
615 "PMI view '{}' is a perspective camera; a sheet draws {ORTHOGRAPHIC} views only",
616 placed.view
617 );
618 return out;
619 }
620 if !placed.projection.eq_ignore_ascii_case(ORTHOGRAPHIC) {
621 out.error = format!("projection '{}' is not drawn on a sheet", placed.projection);
622 return out;
623 }
624
625 let saved_section = derived.is_none() && view.display.section.is_some();
626 let derived = derived.or_else(|| view.section_plane().map(|(point, normal)| {
627 let original = ViewFrame::of(camera);
628 let seed = if dot(original.up, normal).abs() < 0.99 { original.up } else if normal[0].abs() < 0.9 { [1., 0., 0.] } else { [0., 1., 0.] };
629 let right = norm(cross(normal, seed));
630 Derived::Section(SectionSetup { frame: ViewFrame { target: point, right, up: cross(right, normal), forward: normal }, plane: ClipPlane { point, normal }, label: view.name.clone() })
631 }));
632 let frame = match &derived {
633 Some(Derived::Section(setup)) => setup.frame,
634 Some(Derived::Detail(setup)) => setup.frame,
635 None => ViewFrame::of(camera),
636 };
637 out.text_mm = view.display.text_size_pt * MM_PER_POINT;
638 let cap = if out.text_mm > 1e-9 { out.text_mm } else { 12.0 * MM_PER_POINT };
639 let plan = exact_solids(scene, view, report);
644 let clip = match &derived {
645 None => LinesClip::Whole,
646 Some(Derived::Detail(setup)) => LinesClip::Circle { radius: setup.radius },
647 Some(Derived::Section(_)) => LinesClip::Section,
648 };
649 let exact = plan.as_ref().and_then(|plan| exact_lines_for(plan, &frame, placed, clip, mode));
650 out.hidden_line = if exact.is_some() { HiddenLine::Analytic } else { HiddenLine::Mesh }.label().into();
651 match &derived {
652 None => {
653 out.edges = match exact {
654 Some(lines) => lines.runs,
655 None => mesh_lines_for(scene, unposed, view, report, &frame, placed, LinesClip::Whole, None).runs,
656 };
657 out.annotations = annotations(report, view, &frame, placed);
658 out.tolerances = tolerance_sources(report, view, &frame, placed, model.env);
659 }
660 Some(Derived::Detail(setup)) => {
661 let radius_mm = setup.radius * placed.scale;
662 let lines = match exact {
666 Some(lines) => lines.runs,
667 None => mesh_lines_for(scene, unposed, view, report, &frame, placed, clip, None).runs,
668 };
669 out.edges = clip_to_circle(lines, placed.position, radius_mm);
670 out.detail = Some(DetailDrawing {
671 label: setup.label.clone(),
672 centre: placed.position,
673 radius_mm,
674 circle: circle_polyline(placed.position, radius_mm),
675 texts: Vec::new(),
676 });
677 }
678 Some(Derived::Section(setup)) => match exact {
679 Some(ExactLines { runs, cut }) => {
680 let mut edges = runs;
683 edges.extend(cut.iter().cloned());
684 out.edges = edges;
685 out.section = Some(SectionDrawing {
686 label: setup.label.clone(),
687 hatch: hatch(&segments_of(&cut)),
688 cut,
689 texts: Vec::new(),
690 });
691 }
692 None => {
693 let ExactLines { runs, cut: cut_paper } =
694 mesh_lines_for(scene, unposed, view, report, &frame, placed, clip, Some(&setup.plane));
695 let mut edges = runs;
696 edges.extend(cut_paper.iter().cloned());
697 out.edges = edges;
698 out.section = Some(SectionDrawing {
699 label: setup.label.clone(),
700 hatch: hatch(&cut_paper),
701 cut: cut_paper,
702 texts: Vec::new(),
703 });
704 }
705 },
706 }
707 if saved_section {
708 out.annotations = annotations(report, view, &frame, placed);
709 out.tolerances = tolerance_sources(report, view, &frame, placed, model.env);
710 }
711 out.anchors = anchor_points(scene, unposed, view, report, &frame, placed);
712 if let Some(detail) = &out.detail {
719 let reach = detail.radius_mm * (1.0 + 1e-9) + 1e-9;
720 out.anchors.retain(|anchor| {
721 let (dx, dy) = (anchor.at[0] - detail.centre[0], anchor.at[1] - detail.centre[1]);
722 (dx * dx + dy * dy).sqrt() <= reach
723 });
724 }
725 out.bounds = bounds_of(&out, placed);
726 let middle = (out.bounds[0] + out.bounds[2]) * 0.5;
730 let below = out.bounds[3] + cap * 2.0;
731 if let Some(section) = out.section.as_mut() {
732 section.texts.push(SheetText::flat(
733 format!("SECTION {}\u{2014}{}", section.label, section.label),
734 [middle, below],
735 cap,
736 ));
737 out.bounds = bounds_of(&out, placed);
738 }
739 if let Some(detail) = out.detail.as_mut() {
742 detail.texts.push(SheetText::flat(format!("DETAIL {}", detail.label), [middle, below], cap));
743 detail.texts.push(SheetText::flat(
744 format!("SCALE {}", super::frame::ratio_text(placed.scale)),
745 [middle, below + cap * 1.8],
746 cap * 0.8,
747 ));
748 out.bounds = bounds_of(&out, placed);
749 }
750 out
751}
752
753pub fn project_sheet(
759 scene: &RenderScene,
760 sheet: &super::Sheet,
761 views: &[PmiView],
762 report: Option<&brep_kernel::PmiReport>,
763 context: &super::frame::FrameContext,
764) -> SheetDrawing {
765 let (unposed, env) = (Unposed::new(), brep_kernel::Env::default());
766 project_sheet_with(&Model { scene, unposed: &unposed, env: &env }, sheet, views, report, context, &mut LinesMode::Compute)
767}
768
769pub fn project_sheet_with(
773 model: &Model,
774 sheet: &super::Sheet,
775 views: &[PmiView],
776 report: Option<&brep_kernel::PmiReport>,
777 context: &super::frame::FrameContext,
778 mode: &mut LinesMode,
779) -> SheetDrawing {
780 let (width_mm, height_mm) = sheet.millimetres();
781 let mut drawn: Vec<ViewDrawing> = sheet
789 .views
790 .iter()
791 .map(|placed| {
792 let saved = views.iter().find(|view| view.id == placed.view);
793 if placed.section.is_some() || placed.detail.is_some() {
794 ViewDrawing::empty(placed, saved.map(|v| v.name.clone()).unwrap_or_default())
798 } else {
799 project_view_through(model, placed, saved, report.and_then(|report| report.view(&placed.view)), None, mode)
800 }
801 })
802 .collect();
803 for (index, placed) in sheet.views.iter().enumerate() {
804 let Some(circle) = placed.detail.as_ref() else { continue };
805 match detail_setup(sheet, &drawn, circle, views) {
806 Err(error) => drawn[index].error = error,
807 Ok((setup, source, mark)) => {
808 drawn[index] = project_view_through(
809 model,
810 placed,
811 views.iter().find(|view| view.id == placed.view),
812 report.and_then(|report| report.view(&placed.view)),
813 Some(Derived::Detail(setup)),
814 mode,
815 );
816 let mark = DetailMark { id: placed.id.clone(), ..mark };
817 drawn[source].detail_marks.push(mark);
818 let source_placed = &sheet.views[source];
819 drawn[source].bounds = bounds_of(&drawn[source], source_placed);
820 }
821 }
822 }
823 for (index, placed) in sheet.views.iter().enumerate() {
824 let Some(cut) = placed.section.as_ref() else { continue };
825 match section_setup(sheet, &drawn, placed, cut, views) {
826 Err(error) => drawn[index].error = error,
827 Ok((setup, source, mark)) => {
828 drawn[index] = project_view_through(
829 model,
830 placed,
831 views.iter().find(|view| view.id == placed.view),
832 report.and_then(|report| report.view(&placed.view)),
833 Some(Derived::Section(setup)),
834 mode,
835 );
836 drawn[source].section_marks.push(mark);
840 let source_placed = &sheet.views[source];
841 drawn[source].bounds = bounds_of(&drawn[source], source_placed);
842 }
843 }
844 }
845 let dimensions = super::dimension::layout_dimensions(
849 &sheet.dimensions,
850 &drawn,
851 [width_mm * 0.5, height_mm * 0.5],
852 );
853 let ordinates = super::ordinate::layout_ordinates(
854 &sheet.ordinates,
855 &drawn,
856 [width_mm * 0.5, height_mm * 0.5],
857 );
858 SheetDrawing {
859 id: sheet.id.clone(),
860 name: sheet.name.clone(),
861 width_mm,
862 height_mm,
863 frame: super::frame::frame(sheet, context),
864 views: drawn,
865 dimensions,
866 ordinates,
867 }
868}
869
870fn section_setup(
897 sheet: &super::Sheet,
898 drawn: &[ViewDrawing],
899 placed: &PlacedView,
900 cut: &super::SectionCut,
901 views: &[PmiView],
902) -> Result<(SectionSetup, usize, SectionMark), String> {
903 if cut.from.is_empty() || cut.to.is_empty() {
906 return Err("no cutting line yet \u{2014} pick its two anchors on a placed view".into());
907 }
908 let Some(source_index) = sheet.views.iter().position(|v| v.id == cut.source) else {
909 return Err(format!("no placement '{}' to cut through on this sheet", cut.source));
910 };
911 let source_placed = &sheet.views[source_index];
912 if source_placed.section.is_some() {
913 return Err(format!(
914 "'{}' is itself a section; a section of a section is not drawn",
915 cut.source
916 ));
917 }
918 if source_placed.detail.is_some() {
919 return Err(format!(
920 "'{}' is a detail; a section of a detail is not drawn",
921 cut.source
922 ));
923 }
924 let source = &drawn[source_index];
925 if !source.error.is_empty() {
926 return Err(format!("placement '{}' did not project", cut.source));
927 }
928 let (from, to) = (point_of(source, &cut.from)?, point_of(source, &cut.to)?);
929 let span = [to[0] - from[0], to[1] - from[1]];
930 let length = (span[0] * span[0] + span[1] * span[1]).sqrt();
931 if !(length > 1e-9) {
932 return Err(format!(
933 "'{}' and '{}' are the same point; a cutting line needs two",
934 cut.from, cut.to
935 ));
936 }
937 let Some(camera) = views
938 .iter()
939 .find(|view| view.id == source_placed.view)
940 .and_then(|view| view.camera.as_ref())
941 else {
942 return Err(format!("placement '{}' has no captured camera to cut in", cut.source));
943 };
944 let source_frame = ViewFrame::of(camera);
945 if source_placed.scale <= 0.0 {
946 return Err(format!("placement '{}' has no scale", cut.source));
947 }
948 let (p0, p1) = (lift(&source_frame, source_placed, from), lift(&source_frame, source_placed, to));
953 let along = norm(sub(p1, p0));
954 let mut forward = norm(cross(source_frame.forward, along));
955 if forward == [0.0, 0.0, 0.0] {
956 return Err("the cutting line has no direction in this view".into());
957 }
958 if cut.flip {
959 forward = [-forward[0], -forward[1], -forward[2]];
960 }
961 let up = [-source_frame.forward[0], -source_frame.forward[1], -source_frame.forward[2]];
962 let right = norm(cross(forward, up));
963 let target = [(p0[0] + p1[0]) * 0.5, (p0[1] + p1[1]) * 0.5, (p0[2] + p1[2]) * 0.5];
964 let setup = SectionSetup {
965 frame: ViewFrame { target, right, up: cross(right, forward), forward },
966 plane: ClipPlane { point: target, normal: forward },
967 label: cut.label.clone(),
968 };
969 let cap = if source.text_mm > 1e-9 { source.text_mm } else { 12.0 * MM_PER_POINT };
970 Ok((setup, source_index, section_mark(placed, cut, from, to, cap)))
971}
972
973fn section_mark(
977 placed: &PlacedView,
978 cut: &super::SectionCut,
979 from: [f64; 2],
980 to: [f64; 2],
981 cap: f64,
982) -> SectionMark {
983 let span = [to[0] - from[0], to[1] - from[1]];
984 let length = (span[0] * span[0] + span[1] * span[1]).sqrt().max(1e-9);
985 let along = [span[0] / length, span[1] / length];
986 let sign = if cut.flip { -1.0 } else { 1.0 };
991 let look = [-along[1] * sign, along[0] * sign];
992 let tail = cap * 1.5;
993 let arrow = cap * 1.2;
994 let mut lines = vec![vec![
995 [from[0] - along[0] * tail, from[1] - along[1] * tail],
996 [to[0] + along[0] * tail, to[1] + along[1] * tail],
997 ]];
998 let mut texts = Vec::new();
999 for (end, outward) in [(from, [-along[0], -along[1]]), (to, along)] {
1000 let root = [end[0] + outward[0] * tail, end[1] + outward[1] * tail];
1001 let tip = [root[0] + look[0] * arrow * 2.0, root[1] + look[1] * arrow * 2.0];
1002 lines.push(vec![root, tip]);
1006 let across = [-look[1] * arrow * 0.35, look[0] * arrow * 0.35];
1007 lines.push(vec![
1008 tip,
1009 [tip[0] - look[0] * arrow + across[0], tip[1] - look[1] * arrow + across[1]],
1010 [tip[0] - look[0] * arrow - across[0], tip[1] - look[1] * arrow - across[1]],
1011 tip,
1012 ]);
1013 texts.push(SheetText::flat(
1014 cut.label.clone(),
1015 [
1016 root[0] + outward[0] * cap * 1.2 + look[0] * cap * 0.6,
1017 root[1] + outward[1] * cap * 1.2 + look[1] * cap * 0.6,
1018 ],
1019 cap,
1020 ));
1021 }
1022 SectionMark { id: placed.id.clone(), label: cut.label.clone(), lines, texts }
1023}
1024
1025fn point_of(source: &ViewDrawing, reference: &str) -> Result<[f64; 2], String> {
1028 source
1029 .anchors
1030 .iter()
1031 .find(|a| a.anchor == reference)
1032 .map(|a| a.at)
1033 .ok_or_else(|| format!("'{reference}' is not a point of '{}' any more", source.id))
1034}
1035
1036fn lift(frame: &ViewFrame, placed: &PlacedView, p: [f64; 2]) -> [f64; 3] {
1039 let x = (p[0] - placed.position[0]) / placed.scale;
1040 let y = -(p[1] - placed.position[1]) / placed.scale;
1041 [
1042 frame.target[0] + frame.right[0] * x + frame.up[0] * y,
1043 frame.target[1] + frame.right[1] * x + frame.up[1] * y,
1044 frame.target[2] + frame.right[2] * x + frame.up[2] * y,
1045 ]
1046}
1047
1048fn detail_setup(
1070 sheet: &super::Sheet,
1071 drawn: &[ViewDrawing],
1072 circle: &super::DetailCircle,
1073 views: &[PmiView],
1074) -> Result<(DetailSetup, usize, DetailMark), String> {
1075 if circle.centre.is_empty() || circle.rim.is_empty() {
1078 return Err("no detail circle yet \u{2014} pick its centre and a point on its rim on a placed view".into());
1079 }
1080 let Some(source_index) = sheet.views.iter().position(|v| v.id == circle.source) else {
1081 return Err(format!("no placement '{}' to draw a detail of on this sheet", circle.source));
1082 };
1083 let source_placed = &sheet.views[source_index];
1084 if source_placed.detail.is_some() {
1085 return Err(format!(
1086 "'{}' is itself a detail; a detail of a detail is not drawn",
1087 circle.source
1088 ));
1089 }
1090 if source_placed.section.is_some() {
1091 return Err(format!(
1092 "'{}' is a section; a detail of a section is not drawn",
1093 circle.source
1094 ));
1095 }
1096 let source = &drawn[source_index];
1097 if !source.error.is_empty() {
1098 return Err(format!("placement '{}' did not project", circle.source));
1099 }
1100 let (centre, rim) = (point_of(source, &circle.centre)?, point_of(source, &circle.rim)?);
1101 let radius_mm = ((rim[0] - centre[0]).powi(2) + (rim[1] - centre[1]).powi(2)).sqrt();
1102 if !(radius_mm > 1e-9) {
1103 return Err(format!(
1104 "'{}' and '{}' land on the same point of '{}'; a detail circle needs a radius",
1105 circle.centre, circle.rim, circle.source
1106 ));
1107 }
1108 let Some(camera) = views
1109 .iter()
1110 .find(|view| view.id == source_placed.view)
1111 .and_then(|view| view.camera.as_ref())
1112 else {
1113 return Err(format!("placement '{}' has no captured camera to draw a detail of", circle.source));
1114 };
1115 if source_placed.scale <= 0.0 {
1116 return Err(format!("placement '{}' has no scale", circle.source));
1117 }
1118 let source_frame = ViewFrame::of(camera);
1119 let setup = DetailSetup {
1120 frame: ViewFrame { target: lift(&source_frame, source_placed, centre), ..source_frame },
1121 radius: radius_mm / source_placed.scale,
1122 label: circle.label.clone(),
1123 };
1124 let cap = if source.text_mm > 1e-9 { source.text_mm } else { 12.0 * MM_PER_POINT };
1125 let d = std::f64::consts::FRAC_1_SQRT_2;
1129 let reach = radius_mm + cap * 1.5;
1130 let mark = DetailMark {
1131 id: String::new(),
1132 label: circle.label.clone(),
1133 centre,
1134 radius_mm,
1135 lines: vec![circle_polyline(centre, radius_mm)],
1136 texts: vec![SheetText::flat(
1137 circle.label.clone(),
1138 [centre[0] + reach * d, centre[1] - reach * d],
1139 cap,
1140 )],
1141 };
1142 Ok((setup, source_index, mark))
1143}
1144
1145const CIRCLE_SEGMENTS: usize = 96;
1149
1150fn circle_polyline(centre: [f64; 2], radius: f64) -> Vec<[f64; 2]> {
1152 (0..=CIRCLE_SEGMENTS)
1153 .map(|k| {
1154 let t = std::f64::consts::TAU * k as f64 / CIRCLE_SEGMENTS as f64;
1155 [centre[0] + radius * t.cos(), centre[1] + radius * t.sin()]
1156 })
1157 .collect()
1158}
1159
1160fn clip_to_circle(lines: Vec<Vec<[f64; 2]>>, centre: [f64; 2], radius: f64) -> Vec<Vec<[f64; 2]>> {
1169 let mut out: Vec<Vec<[f64; 2]>> = Vec::new();
1170 let flush = |current: &mut Vec<[f64; 2]>, out: &mut Vec<Vec<[f64; 2]>>| {
1171 if current.len() >= 2 {
1172 out.push(std::mem::take(current));
1173 } else {
1174 current.clear();
1175 }
1176 };
1177 for line in lines {
1178 let mut current: Vec<[f64; 2]> = Vec::new();
1179 for pair in line.windows(2) {
1180 let (p, q) = (pair[0], pair[1]);
1181 let d = [q[0] - p[0], q[1] - p[1]];
1182 let f = [p[0] - centre[0], p[1] - centre[1]];
1183 let a = d[0] * d[0] + d[1] * d[1];
1184 let c = f[0] * f[0] + f[1] * f[1] - radius * radius;
1185 let span = if a < 1e-24 {
1187 (c <= 0.0).then_some((0.0, 1.0))
1188 } else {
1189 let b = 2.0 * (f[0] * d[0] + f[1] * d[1]);
1190 let disc = b * b - 4.0 * a * c;
1191 if disc < 0.0 {
1192 None
1193 } else {
1194 let root = disc.sqrt();
1195 let (t0, t1) = (((-b - root) / (2.0 * a)).max(0.0), ((-b + root) / (2.0 * a)).min(1.0));
1196 (t1 > t0).then_some((t0, t1))
1197 }
1198 };
1199 let at = |t: f64| [p[0] + d[0] * t, p[1] + d[1] * t];
1200 match span {
1201 None => flush(&mut current, &mut out),
1202 Some((t0, t1)) => {
1203 if t0 <= 0.0 {
1204 if current.is_empty() {
1205 current.push(p);
1206 }
1207 if t1 >= 1.0 {
1208 current.push(q);
1209 } else {
1210 current.push(at(t1));
1211 flush(&mut current, &mut out);
1212 }
1213 } else {
1214 flush(&mut current, &mut out);
1215 current.push(at(t0));
1216 if t1 >= 1.0 {
1217 current.push(q);
1218 } else {
1219 current.push(at(t1));
1220 flush(&mut current, &mut out);
1221 }
1222 }
1223 }
1224 }
1225 }
1226 flush(&mut current, &mut out);
1227 }
1228 out
1229}
1230
1231fn bounds_of(view: &ViewDrawing, placed: &PlacedView) -> [f64; 4] {
1232 let mut bounds = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
1233 let mut add = |p: [f64; 2]| {
1234 bounds[0] = bounds[0].min(p[0]);
1235 bounds[1] = bounds[1].min(p[1]);
1236 bounds[2] = bounds[2].max(p[0]);
1237 bounds[3] = bounds[3].max(p[1]);
1238 };
1239 for polyline in &view.edges {
1240 for point in polyline {
1241 add(*point);
1242 }
1243 }
1244 for annotation in &view.annotations {
1245 for polyline in &annotation.lines {
1246 for point in polyline {
1247 add(*point);
1248 }
1249 }
1250 for text in &annotation.texts {
1251 add(text.anchor);
1252 }
1253 }
1254 if let Some(section) = &view.section {
1255 for polyline in section.hatch.iter().chain(section.cut.iter()) {
1256 for point in polyline {
1257 add(*point);
1258 }
1259 }
1260 for text in §ion.texts {
1261 add(text.anchor);
1262 }
1263 }
1264 for mark in &view.section_marks {
1265 for polyline in &mark.lines {
1266 for point in polyline {
1267 add(*point);
1268 }
1269 }
1270 for text in &mark.texts {
1271 add(text.anchor);
1272 }
1273 }
1274 if let Some(detail) = &view.detail {
1275 for point in &detail.circle {
1276 add(*point);
1277 }
1278 for text in &detail.texts {
1279 add(text.anchor);
1280 }
1281 }
1282 for mark in &view.detail_marks {
1283 for polyline in &mark.lines {
1284 for point in polyline {
1285 add(*point);
1286 }
1287 }
1288 for text in &mark.texts {
1289 add(text.anchor);
1290 }
1291 }
1292 if bounds[0] > bounds[2] {
1293 [placed.position[0], placed.position[1], placed.position[0], placed.position[1]]
1294 } else {
1295 bounds
1296 }
1297}
1298
1299#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
1306pub struct SolidPose {
1307 pub translate: [f64; 3],
1308 #[serde(rename = "rotateDeg")]
1309 pub rotate_deg: [f64; 3],
1310 pub scale: [f64; 3],
1311 pub center: [f64; 3],
1312}
1313
1314impl SolidPose {
1315 pub fn apply(&self, p: [f64; 3]) -> [f64; 3] {
1316 let (center, scale, translate) = (self.center, self.scale, self.translate);
1317 let local = [(p[0] - center[0]) * scale[0], (p[1] - center[1]) * scale[1], (p[2] - center[2]) * scale[2]];
1318 let rotated = crate::engine_state::rotate_euler_xyz_f64(local, self.rotate_deg);
1319 [
1320 rotated[0] + center[0] + translate[0],
1321 rotated[1] + center[1] + translate[1],
1322 rotated[2] + center[2] + translate[2],
1323 ]
1324 }
1325
1326 fn identity(&self) -> String {
1328 let bits = |v: &[f64; 3]| format!("{:x},{:x},{:x}", v[0].to_bits(), v[1].to_bits(), v[2].to_bits());
1329 format!(
1330 "pose[{}|{}|{}|{}];",
1331 bits(&self.translate),
1332 bits(&self.rotate_deg),
1333 bits(&self.scale),
1334 bits(&self.center)
1335 )
1336 }
1337}
1338
1339pub(crate) fn poses_of(report: Option<&PmiViewReport>, name: &str) -> Vec<SolidPose> {
1343 let Some(report) = report else { return Vec::new() };
1344 report
1345 .annotations
1346 .iter()
1347 .filter(|row| row.enabled && row.status == PmiStatus::Ok)
1348 .filter_map(|row| match &row.geometry {
1349 brep_kernel::PmiGeometry::Explode { solids, translate, rotate_deg, scale, center, .. }
1350 if solids.iter().any(|target| target == name) =>
1351 {
1352 Some(SolidPose { translate: *translate, rotate_deg: *rotate_deg, scale: *scale, center: *center })
1353 }
1354 _ => None,
1355 })
1356 .collect()
1357}
1358
1359#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1363pub struct LinesSolid {
1364 pub name: String,
1365 pub handle: u32,
1366 pub fingerprint: u64,
1367 pub poses: Vec<SolidPose>,
1368}
1369
1370impl LinesSolid {
1371 fn identity(&self) -> String {
1376 let mut identity = format!("{}#{:016x};", self.name, self.fingerprint);
1377 for pose in &self.poses {
1378 identity.push_str(&pose.identity());
1379 }
1380 identity
1381 }
1382}
1383
1384struct ExactPlan<'a> {
1388 solids: Vec<LinesSolid>,
1389 sources: Vec<&'a brep_kernel::BrepSolid>,
1391 each: Vec<String>,
1393 identity: String,
1394}
1395
1396impl ExactPlan<'_> {
1397 fn posed(&self) -> Vec<std::borrow::Cow<'_, brep_kernel::BrepSolid>> {
1404 self.solids
1405 .iter()
1406 .zip(&self.sources)
1407 .map(|(solid, source)| posed_solid(std::borrow::Cow::Borrowed(*source), &solid.poses))
1408 .collect()
1409 }
1410}
1411
1412fn posed_solid<'a>(
1413 solid: std::borrow::Cow<'a, brep_kernel::BrepSolid>,
1414 poses: &[SolidPose],
1415) -> std::borrow::Cow<'a, brep_kernel::BrepSolid> {
1416 poses.iter().fold(solid, |solid, pose| std::borrow::Cow::Owned(pose_solid(&solid, &|p| pose.apply(p))))
1417}
1418
1419fn exact_solids<'a>(scene: &'a RenderScene, view: &PmiView, report: Option<&PmiViewReport>) -> Option<ExactPlan<'a>> {
1422 let mut plan = ExactPlan { solids: Vec::new(), sources: Vec::new(), each: Vec::new(), identity: String::new() };
1423 for solid in drawn_solids(scene, view) {
1424 let (brep, fingerprint) = scene.exact_solid_fingerprinted(&solid.name)?;
1425 let planned = LinesSolid {
1426 name: solid.name.clone(),
1427 handle: solid.source_handle,
1428 fingerprint,
1429 poses: poses_of(report, &solid.name),
1430 };
1431 plan.each.push(planned.identity());
1432 plan.solids.push(planned);
1433 plan.sources.push(brep);
1434 }
1435 plan.identity = plan.each.concat();
1436 Some(plan)
1437}
1438
1439#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
1446pub enum LinesClip {
1447 Whole,
1448 Circle { radius: f64 },
1449 Section,
1450}
1451
1452impl LinesClip {
1453 fn key(&self) -> String {
1455 match self {
1456 LinesClip::Whole => "whole".into(),
1457 LinesClip::Circle { radius } => format!("circle {:x}", radius.to_bits()),
1458 LinesClip::Section => "section".into(),
1459 }
1460 }
1461}
1462
1463#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1467pub struct LinesJob {
1468 pub key: String,
1470 pub placement: String,
1473 pub solids: Vec<LinesSolid>,
1474 pub frame: ViewFrame,
1475 pub placed: PlacedView,
1477 pub clip: LinesClip,
1478}
1479
1480pub enum LinesMode<'a> {
1482 Compute,
1485 Defer(&'a mut dyn FnMut(LinesJob) -> Deferred),
1490}
1491
1492pub enum Deferred {
1494 Lines(ExactLines),
1495 Pending,
1496 Here,
1497}
1498
1499pub fn run_lines_job(job: &LinesJob) -> Result<ExactLines, String> {
1505 let mut posed = Vec::with_capacity(job.solids.len());
1506 for solid in &job.solids {
1507 let brep = brep_kernel::registered_solid_clone(solid.handle)
1508 .map_err(|error| format!("'{}' (handle {}): {error}", solid.name, solid.handle))?;
1509 if crate::scene::brep_fingerprint(&brep) != solid.fingerprint {
1510 return Err(format!("'{}' changed under handle {}", solid.name, solid.handle));
1511 }
1512 posed.push(posed_solid(std::borrow::Cow::Owned(brep), &solid.poses).into_owned());
1513 }
1514 let solids: Vec<&brep_kernel::BrepSolid> = posed.iter().collect();
1515 let each: Vec<String> = job.solids.iter().map(LinesSolid::identity).collect();
1516 Ok(compute_exact_lines(&solids, &each, &job.frame, &job.placed, job.clip))
1517}
1518
1519fn compute_exact_lines(
1522 solids: &[&brep_kernel::BrepSolid],
1523 each: &[String],
1524 frame: &ViewFrame,
1525 origin: &PlacedView,
1526 clip: LinesClip,
1527) -> ExactLines {
1528 let trims = solid_trims(each, solids);
1529 let trims: Vec<&super::hlr::SolidTrims> = trims.iter().map(|t| t.as_ref()).collect();
1530 match clip {
1531 LinesClip::Whole => ExactLines {
1532 runs: super::hlr::visible_edges_with(solids, &trims, frame, origin, &super::hlr::Clip::Whole).runs,
1533 cut: Vec::new(),
1534 },
1535 LinesClip::Circle { radius } => ExactLines {
1536 runs: super::hlr::visible_edges_with(solids, &trims, frame, origin, &super::hlr::Clip::Circle { radius }).runs,
1537 cut: Vec::new(),
1538 },
1539 LinesClip::Section => {
1540 let cut = super::hlr::section_curves_with(solids, &trims, frame, origin);
1544 let clip = super::hlr::Clip::Beyond { cap: &cut };
1545 ExactLines { runs: super::hlr::visible_edges_with(solids, &trims, frame, origin, &clip).runs, cut }
1546 }
1547 }
1548}
1549
1550fn exact_lines_for(
1554 plan: &ExactPlan,
1555 frame: &ViewFrame,
1556 placed: &PlacedView,
1557 clip: LinesClip,
1558 mode: &mut LinesMode,
1559) -> Option<ExactLines> {
1560 let key = exact_key(&plan.identity, frame, placed.scale, &clip.key());
1561 let origin = PlacedView { position: [0.0, 0.0], ..placed.clone() };
1562 let relative = match cached_exact_lines(&key) {
1563 Some(lines) => lines,
1564 None => {
1565 let deferred = match mode {
1566 LinesMode::Compute => Deferred::Here,
1567 LinesMode::Defer(defer) => defer(LinesJob {
1568 key: key.clone(),
1569 placement: placed.id.clone(),
1570 solids: plan.solids.clone(),
1571 frame: *frame,
1572 placed: origin.clone(),
1573 clip,
1574 }),
1575 };
1576 let lines = match deferred {
1577 Deferred::Lines(lines) => lines,
1578 Deferred::Pending => return None,
1579 Deferred::Here => {
1580 let posed = plan.posed();
1581 let solids: Vec<&brep_kernel::BrepSolid> = posed.iter().map(|solid| solid.as_ref()).collect();
1582 compute_exact_lines(&solids, &plan.each, frame, &origin, clip)
1583 }
1584 };
1585 store_exact_lines(key, lines.clone());
1586 lines
1587 }
1588 };
1589 let shift = |lines: Vec<Vec<[f64; 2]>>| -> Vec<Vec<[f64; 2]>> {
1590 lines
1591 .into_iter()
1592 .map(|line| line.into_iter().map(|p| [p[0] + placed.position[0], p[1] + placed.position[1]]).collect())
1593 .collect()
1594 };
1595 Some(ExactLines { runs: shift(relative.runs), cut: shift(relative.cut) })
1596}
1597
1598const TRIM_CACHE_SOLIDS: usize = EXACT_CACHE_ENTRIES;
1601
1602thread_local! {
1603 static TRIM_CACHE: std::cell::RefCell<std::collections::VecDeque<(String, std::rc::Rc<super::hlr::SolidTrims>)>> =
1608 const { std::cell::RefCell::new(std::collections::VecDeque::new()) };
1609}
1610
1611
1612
1613fn solid_trims(identities: &[String], solids: &[&brep_kernel::BrepSolid]) -> Vec<std::rc::Rc<super::hlr::SolidTrims>> {
1615 TRIM_CACHE.with(|cache| {
1616 let mut cache = cache.borrow_mut();
1617 identities
1618 .iter()
1619 .zip(solids)
1620 .map(|(identity, solid)| {
1621 let trims = match cache.iter().position(|(key, _)| key == identity) {
1622 Some(index) => cache.remove(index).map(|(_, trims)| trims).unwrap(),
1623 None => std::rc::Rc::new(super::hlr::SolidTrims::of(solid)),
1624 };
1625 cache.push_back((identity.clone(), trims.clone()));
1626 while cache.len() > TRIM_CACHE_SOLIDS {
1627 cache.pop_front();
1628 }
1629 trims
1630 })
1631 .collect()
1632 })
1633}
1634
1635#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
1639pub struct ExactLines {
1640 pub runs: Vec<Vec<[f64; 2]>>,
1641 pub cut: Vec<Vec<[f64; 2]>>,
1642}
1643
1644const EXACT_CACHE_ENTRIES: usize = 64;
1646
1647thread_local! {
1648 static EXACT_CACHE: std::cell::RefCell<std::collections::VecDeque<(String, ExactLines)>> =
1655 const { std::cell::RefCell::new(std::collections::VecDeque::new()) };
1656}
1657
1658pub(crate) fn cached_exact_lines(key: &str) -> Option<ExactLines> {
1660 EXACT_CACHE.with(|cache| cache.borrow().iter().find(|(k, _)| k == key).map(|(_, lines)| lines.clone()))
1661}
1662
1663pub(crate) fn store_exact_lines(key: String, lines: ExactLines) {
1666 EXACT_CACHE.with(|cache| {
1667 let mut cache = cache.borrow_mut();
1668 cache.retain(|(k, _)| *k != key);
1669 cache.push_back((key, lines));
1670 while cache.len() > EXACT_CACHE_ENTRIES {
1671 cache.pop_front();
1672 }
1673 });
1674}
1675
1676fn exact_key(identity: &str, frame: &ViewFrame, scale: f64, clip: &str) -> String {
1678 let bits = |v: [f64; 3]| format!("{:x},{:x},{:x}", v[0].to_bits(), v[1].to_bits(), v[2].to_bits());
1679 format!(
1680 "{identity}|{}|{}|{}|{}|{:x}|{clip}",
1681 bits(frame.target),
1682 bits(frame.right),
1683 bits(frame.up),
1684 bits(frame.forward),
1685 scale.to_bits()
1686 )
1687}
1688
1689fn pose_solid(solid: &brep_kernel::BrepSolid, pose: &dyn Fn([f64; 3]) -> [f64; 3]) -> brep_kernel::BrepSolid {
1696 let map = |cp: &brep_kernel::Vec4| -> brep_kernel::Vec4 {
1697 let w = if cp.w != 0.0 { cp.w } else { 1.0 };
1698 let p = pose([cp.x / w, cp.y / w, cp.z / w]);
1699 brep_kernel::Vec4 { x: p[0] * w, y: p[1] * w, z: p[2] * w, w: cp.w }
1700 };
1701 let mut out = solid.clone();
1702 for vertex in &mut out.vertices {
1703 let p = pose([vertex.point.x, vertex.point.y, vertex.point.z]);
1704 vertex.point = brep_kernel::Vec3::new(p[0], p[1], p[2]);
1705 }
1706 for edge in &mut out.edges {
1707 let points = edge.curve.control_points.iter().map(map).collect();
1708 if let Ok(curve) = brep_kernel::NurbsCurve::new(edge.curve.degree, edge.curve.knots.clone(), points) {
1709 edge.curve = curve;
1710 }
1711 }
1712 for shell in &mut out.shells {
1713 for face in &mut shell.faces {
1714 let s = &face.surface;
1715 let points = s.control_points.iter().map(|row| row.iter().map(map).collect()).collect();
1716 if let Ok(surface) =
1717 brep_kernel::NurbsSurface::new(s.degree_u, s.degree_v, s.knots_u.clone(), s.knots_v.clone(), points)
1718 {
1719 face.surface = surface;
1720 }
1721 }
1722 }
1723 out
1724}
1725
1726fn segments_of(lines: &[Vec<[f64; 2]>]) -> Vec<Vec<[f64; 2]>> {
1728 lines.iter().flat_map(|line| line.windows(2).map(|w| vec![w[0], w[1]])).collect()
1729}
1730
1731fn drawn_solids<'a>(scene: &'a RenderScene, view: &PmiView) -> Vec<&'a crate::scene::SolidDisplay> {
1739 scene
1740 .solids()
1741 .iter()
1742 .filter(|solid| solid.visible && !solid.is_sketch)
1743 .filter(|solid| !view.display.hidden.iter().any(|name| name == &solid.name))
1744 .filter(|solid| solid.mesh.indices.len() >= 3)
1745 .collect()
1746}
1747
1748pub fn sheet_solid_names(scene: &RenderScene, sheet: &super::Sheet, views: &[PmiView]) -> Vec<String> {
1751 let mut names: Vec<String> = Vec::new();
1752 for placed in &sheet.views {
1753 let Some(view) = views.iter().find(|view| view.id == placed.view) else { continue };
1754 for solid in drawn_solids(scene, view) {
1755 if !names.contains(&solid.name) {
1756 names.push(solid.name.clone());
1757 }
1758 }
1759 }
1760 names
1761}
1762
1763fn drawn_displays<'a>(scene: &'a RenderScene, unposed: &'a Unposed, view: &PmiView) -> Vec<&'a crate::scene::SolidDisplay> {
1767 drawn_solids(scene, view).into_iter().map(|solid| unposed.get(&solid.name).unwrap_or(solid)).collect()
1768}
1769
1770
1771fn mesh_solids(displays: &[&crate::scene::SolidDisplay], report: Option<&PmiViewReport>) -> Vec<super::hlr::MeshSolid> {
1787 let world = |p: &[f32; 3]| [p[0] as f64, p[1] as f64, p[2] as f64];
1788 displays
1789 .iter()
1790 .map(|solid| {
1791 let poses = poses_of(report, &solid.name);
1792 let pose = |p: [f64; 3]| poses.iter().fold(p, |p, pose| pose.apply(p));
1793 let count = solid.mesh.positions.len();
1794 let (triangles, faces) = solid
1795 .mesh
1796 .indices
1797 .chunks_exact(3)
1798 .enumerate()
1799 .filter(|(_, c)| c.iter().all(|i| (*i as usize) < count))
1800 .map(|(t, c)| ([c[0], c[1], c[2]], solid.mesh.face_ids.get(t).copied().unwrap_or(u32::MAX)))
1801 .unzip();
1802 let bits = |p: [f32; 3]| [p[0].to_bits(), p[1].to_bits(), p[2].to_bits()];
1803 let mut at: std::collections::HashMap<[u32; 3], Option<u64>> = std::collections::HashMap::new();
1804 for vertex in &solid.vertices {
1805 let p = vertex.position;
1806 at.entry(bits([p[0] as f32, p[1] as f32, p[2] as f32]))
1807 .and_modify(|id| *id = None)
1808 .or_insert(Some(vertex.topo_id));
1809 }
1810 let vertex_of = |p: &[f32; 3]| at.get(&bits(*p)).copied().flatten();
1811 super::hlr::MeshSolid {
1812 positions: solid.mesh.positions.iter().map(|p| pose(world(p))).collect(),
1813 triangles,
1814 faces,
1815 edges: solid
1816 .edges
1817 .iter()
1818 .filter(|edge| !edge.aux && !edge.centerline && edge.polyline.len() >= 2)
1819 .map(|edge| super::hlr::MeshEdge {
1820 points: edge.polyline.iter().map(|p| pose(world(p))).collect(),
1821 ends: vertex_of(&edge.polyline[0]).zip(vertex_of(edge.polyline.last().unwrap())),
1822 })
1823 .collect(),
1824 }
1825 })
1826 .collect()
1827}
1828
1829const MESH_CACHE_ENTRIES: usize = 16;
1831
1832thread_local! {
1833 static MESH_CACHE: std::cell::RefCell<std::collections::VecDeque<(String, ExactLines)>> =
1840 const { std::cell::RefCell::new(std::collections::VecDeque::new()) };
1841}
1842
1843#[allow(clippy::too_many_arguments)]
1846fn mesh_lines_for(
1847 scene: &RenderScene,
1848 unposed: &Unposed,
1849 view: &PmiView,
1850 report: Option<&PmiViewReport>,
1851 frame: &ViewFrame,
1852 placed: &PlacedView,
1853 clip: LinesClip,
1854 plane: Option<&ClipPlane>,
1855) -> ExactLines {
1856 let displays = drawn_displays(scene, unposed, view);
1857 let mut identity = String::from("mesh|");
1858 for display in &displays {
1859 identity.push_str(&format!("{}@{};", display.name, display.revision));
1860 for pose in poses_of(report, &display.name) {
1861 identity.push_str(&pose.identity());
1862 }
1863 }
1864 let bits = |v: [f64; 3]| format!("{:x},{:x},{:x}", v[0].to_bits(), v[1].to_bits(), v[2].to_bits());
1865 let plane_key = plane.map(|plane| format!("{}|{}", bits(plane.point), bits(plane.normal))).unwrap_or_default();
1866 let key = exact_key(&identity, frame, placed.scale, &format!("{}{plane_key}", clip.key()));
1867 let hit = MESH_CACHE.with(|cache| cache.borrow().iter().find(|(k, _)| *k == key).map(|(_, lines)| lines.clone()));
1868 let relative = match hit {
1869 Some(lines) => lines,
1870 None => {
1871 let origin = PlacedView { position: [0.0, 0.0], ..placed.clone() };
1872 let meshes = mesh_solids(&displays, report);
1873 let lines = match (clip, plane) {
1874 (LinesClip::Section, Some(plane)) => {
1875 let cut: Vec<Vec<[f64; 2]>> = mesh_cut(&meshes, plane)
1876 .iter()
1877 .map(|segment| {
1878 segment
1879 .iter()
1880 .map(|p| {
1881 let v = frame.to_view(*p);
1882 to_paper(&origin, v[0], v[1])
1883 })
1884 .collect()
1885 })
1886 .collect();
1887 let beyond = super::hlr::Clip::Beyond { cap: &cut };
1888 ExactLines { runs: super::hlr::visible_mesh_lines(&meshes, frame, &origin, &beyond).runs, cut }
1889 }
1890 (LinesClip::Circle { radius }, _) => ExactLines {
1891 runs: super::hlr::visible_mesh_lines(&meshes, frame, &origin, &super::hlr::Clip::Circle { radius }).runs,
1892 cut: Vec::new(),
1893 },
1894 _ => ExactLines {
1895 runs: super::hlr::visible_mesh_lines(&meshes, frame, &origin, &super::hlr::Clip::Whole).runs,
1896 cut: Vec::new(),
1897 },
1898 };
1899 MESH_CACHE.with(|cache| {
1900 let mut cache = cache.borrow_mut();
1901 cache.push_back((key, lines.clone()));
1902 while cache.len() > MESH_CACHE_ENTRIES {
1903 cache.pop_front();
1904 }
1905 });
1906 lines
1907 }
1908 };
1909 let shift = |lines: Vec<Vec<[f64; 2]>>| -> Vec<Vec<[f64; 2]>> {
1910 lines
1911 .into_iter()
1912 .map(|line| line.into_iter().map(|p| [p[0] + placed.position[0], p[1] + placed.position[1]]).collect())
1913 .collect()
1914 };
1915 ExactLines { runs: shift(relative.runs), cut: shift(relative.cut) }
1916}
1917
1918#[derive(Debug, Clone, Copy)]
1933pub struct ClipPlane {
1934 pub point: [f64; 3],
1935 pub normal: [f64; 3],
1936}
1937
1938fn mesh_cut(meshes: &[super::hlr::MeshSolid], plane: &ClipPlane) -> Vec<[[f64; 3]; 2]> {
1943 let mut extent: f64 = 0.0;
1947 for mesh in meshes {
1948 for p in &mesh.positions {
1949 extent = extent.max(dot(sub(*p, plane.point), plane.normal).abs());
1950 }
1951 }
1952 let eps = (extent * 1e-9).max(f64::MIN_POSITIVE);
1953
1954 let mut cut: Vec<[[f64; 3]; 2]> = Vec::new();
1955 for mesh in meshes {
1956 let side: Vec<f64> = mesh
1957 .positions
1958 .iter()
1959 .map(|p| dot(sub(*p, plane.point), plane.normal))
1960 .collect();
1961 for tri in &mesh.triangles {
1962 let corners = [tri[0] as usize, tri[1] as usize, tri[2] as usize];
1963 let d = [side[corners[0]], side[corners[1]], side[corners[2]]];
1964 if d.iter().all(|v| *v < -eps) || d.iter().all(|v| *v > eps) {
1965 continue; }
1967 let mut kept = 0;
1970 let mut on_plane: Vec<[f64; 3]> = Vec::with_capacity(2);
1971 for k in 0..3 {
1972 let (i, j) = (k, (k + 1) % 3);
1973 let (pi, pj) = (mesh.positions[corners[i]], mesh.positions[corners[j]]);
1974 let (di, dj) = (d[i], d[j]);
1975 if di >= -eps {
1976 kept += 1;
1977 if di.abs() <= eps {
1978 on_plane.push(pi);
1979 }
1980 }
1981 if (di > eps && dj < -eps) || (di < -eps && dj > eps) {
1982 let t = di / (di - dj);
1983 kept += 1;
1984 on_plane.push([
1985 pi[0] + (pj[0] - pi[0]) * t,
1986 pi[1] + (pj[1] - pi[1]) * t,
1987 pi[2] + (pj[2] - pi[2]) * t,
1988 ]);
1989 }
1990 }
1991 if kept >= 3 && on_plane.len() == 2 {
2005 cut.push([on_plane[0], on_plane[1]]);
2006 }
2007 }
2008 }
2009 cut
2010}
2011
2012fn hatch(cut: &[Vec<[f64; 2]>]) -> Vec<Vec<[f64; 2]>> {
2022 if cut.is_empty() {
2023 return Vec::new();
2024 }
2025 let d = std::f64::consts::FRAC_1_SQRT_2;
2028 let u = [d, -d];
2029 let n = [d, d];
2030 let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
2031 for segment in cut {
2032 for p in segment {
2033 let c = p[0] * n[0] + p[1] * n[1];
2034 lo = lo.min(c);
2035 hi = hi.max(c);
2036 }
2037 }
2038 if !lo.is_finite() || hi <= lo {
2039 return Vec::new();
2040 }
2041 let mut spacing = HATCH_SPACING_MM;
2042 while (hi - lo) / spacing > MAX_HATCH_LINES as f64 {
2043 spacing *= 2.0;
2044 }
2045 let first = (lo / spacing).ceil() * spacing;
2048 let mut out = Vec::new();
2049 let mut c = first;
2050 while c <= hi {
2051 let mut crossings: Vec<f64> = Vec::new();
2052 for segment in cut {
2053 let (a, b) = (segment[0], segment[segment.len() - 1]);
2054 let (ca, cb) = (a[0] * n[0] + a[1] * n[1], b[0] * n[0] + b[1] * n[1]);
2055 if (ca <= c) == (cb <= c) {
2058 continue;
2059 }
2060 let t = (c - ca) / (cb - ca);
2061 let hit = [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
2062 crossings.push(hit[0] * u[0] + hit[1] * u[1]);
2063 }
2064 crossings.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
2065 for pair in crossings.chunks_exact(2) {
2066 if pair[1] - pair[0] < 1e-9 {
2067 continue;
2068 }
2069 out.push(vec![
2070 [u[0] * pair[0] + n[0] * c, u[1] * pair[0] + n[1] * c],
2071 [u[0] * pair[1] + n[0] * c, u[1] * pair[1] + n[1] * c],
2072 ]);
2073 }
2074 c += spacing;
2075 }
2076 out
2077}
2078
2079const CIRCLE_MIN_SPAN_DEG: f64 = 20.0;
2088
2089const CIRCLE_FIT_TOLERANCE: f64 = 1e-4;
2094
2095const STRAIGHT_TOLERANCE: f64 = 1e-4;
2100
2101fn anchor_points(
2109 scene: &RenderScene,
2110 unposed: &Unposed,
2111 view: &PmiView,
2112 report: Option<&PmiViewReport>,
2113 frame: &ViewFrame,
2114 placed: &PlacedView,
2115) -> Vec<AnchorPoint> {
2116 let project = |p: [f64; 3]| -> [f64; 2] {
2117 let v = frame.to_view(p);
2118 to_paper(placed, v[0], v[1])
2119 };
2120 let mut out = Vec::new();
2121 for solid in drawn_displays(scene, unposed, view) {
2122 let poses = poses_of(report, &solid.name);
2125 let pose = |p: [f64; 3]| poses.iter().fold(p, |p, pose| pose.apply(p));
2126 for vertex in &solid.vertices {
2127 out.push(AnchorPoint {
2128 anchor: super::dimension::SheetAnchor::vertex(&placed.id, &solid.name, vertex.topo_id)
2129 .to_ref(),
2130 kind: super::dimension::VERTEX.to_string(),
2131 at: project(pose(vertex.position)),
2132 model: pose(vertex.position),
2133 dir: None,
2134 radius: None,
2135 u: None,
2136 v: None,
2137 });
2138 }
2139 for edge in &solid.edges {
2140 if edge.name.is_empty() || edge.aux || edge.centerline || edge.polyline.len() < 2 {
2144 continue;
2145 }
2146 let points: Vec<[f64; 3]> = edge
2147 .polyline
2148 .iter()
2149 .map(|p| pose([p[0] as f64, p[1] as f64, p[2] as f64]))
2150 .collect();
2151 let dir = straight_chord(&points).and_then(|d| {
2156 let paper = [dot(d, frame.right), -dot(d, frame.up)];
2157 let length = (paper[0] * paper[0] + paper[1] * paper[1]).sqrt();
2158 (length > 1e-9).then(|| [paper[0] / length, paper[1] / length])
2159 });
2160 for fraction in [0.0, 0.5, 1.0] {
2161 let Some(point) = along(&points, fraction) else { continue };
2162 out.push(AnchorPoint {
2163 anchor: super::dimension::SheetAnchor::edge(
2164 &placed.id,
2165 &solid.name,
2166 &edge.name,
2167 fraction,
2168 )
2169 .to_ref(),
2170 kind: super::dimension::EDGE.to_string(),
2171 at: project(point),
2172 model: point,
2173 dir,
2174 radius: None,
2175 u: None,
2176 v: None,
2177 });
2178 }
2179 if let Some((centre, axis, radius)) = fit_circle(&points) {
2180 let u3 = norm(sub(points[0], centre));
2184 let v3 = cross(axis, u3);
2185 let image = |d: [f64; 3]| -> [f64; 2] {
2186 [
2187 dot(d, frame.right) * radius * placed.scale,
2188 -dot(d, frame.up) * radius * placed.scale,
2189 ]
2190 };
2191 out.push(AnchorPoint {
2192 anchor: super::dimension::SheetAnchor::circle(&placed.id, &solid.name, &edge.name)
2193 .to_ref(),
2194 kind: super::dimension::CIRCLE.to_string(),
2195 at: project(centre),
2196 model: centre,
2197 dir: None,
2198 radius: Some(radius),
2199 u: Some(image(u3)),
2200 v: Some(image(v3)),
2201 });
2202 }
2203 }
2204 }
2205 out
2206}
2207
2208fn straight_chord(points: &[[f64; 3]]) -> Option<[f64; 3]> {
2216 let (first, last) = (*points.first()?, *points.last()?);
2217 let span = sub(last, first);
2218 let length = dot(span, span).sqrt();
2219 if length < 1e-12 {
2220 return None;
2222 }
2223 let direction = [span[0] / length, span[1] / length, span[2] / length];
2224 let bar = length * STRAIGHT_TOLERANCE;
2225 for point in points {
2226 let offset = sub(*point, first);
2227 let along = dot(offset, direction);
2228 let away = [
2229 offset[0] - direction[0] * along,
2230 offset[1] - direction[1] * along,
2231 offset[2] - direction[2] * along,
2232 ];
2233 if dot(away, away).sqrt() > bar {
2234 return None;
2235 }
2236 }
2237 Some(direction)
2238}
2239
2240fn along(points: &[[f64; 3]], fraction: f64) -> Option<[f64; 3]> {
2242 if points.len() < 2 {
2243 return None;
2244 }
2245 let steps: Vec<f64> = points
2246 .windows(2)
2247 .map(|pair| dot(sub(pair[1], pair[0]), sub(pair[1], pair[0])).sqrt())
2248 .collect();
2249 let total: f64 = steps.iter().sum();
2250 if !(total > 0.0) {
2251 return Some(points[0]);
2252 }
2253 let target = total * fraction.clamp(0.0, 1.0);
2254 let mut walked = 0.0;
2255 for (index, step) in steps.iter().enumerate() {
2256 if walked + step >= target || index + 1 == steps.len() {
2257 let t = if *step > 0.0 { ((target - walked) / step).clamp(0.0, 1.0) } else { 0.0 };
2258 let (a, b) = (points[index], points[index + 1]);
2259 return Some([
2260 a[0] + (b[0] - a[0]) * t,
2261 a[1] + (b[1] - a[1]) * t,
2262 a[2] + (b[2] - a[2]) * t,
2263 ]);
2264 }
2265 walked += step;
2266 }
2267 points.last().copied()
2268}
2269
2270fn fit_circle(points: &[[f64; 3]]) -> Option<([f64; 3], [f64; 3], f64)> {
2279 if points.len() < 4 {
2280 return None;
2281 }
2282 let last = points.len() - 1;
2283 let (a, b, c) = (points[0], points[last / 3], points[last * 2 / 3]);
2284 let (ab, ac) = (sub(b, a), sub(c, a));
2285 let n = cross(ab, ac);
2286 let n2 = dot(n, n);
2287 if n2 < 1e-24 {
2288 return None;
2289 }
2290 let centre = {
2292 let (ab2, ac2) = (dot(ab, ab), dot(ac, ac));
2293 let to = cross(
2294 [
2295 ab2 * ac[0] - ac2 * ab[0],
2296 ab2 * ac[1] - ac2 * ab[1],
2297 ab2 * ac[2] - ac2 * ab[2],
2298 ],
2299 n,
2300 );
2301 [
2302 a[0] + to[0] / (2.0 * n2),
2303 a[1] + to[1] / (2.0 * n2),
2304 a[2] + to[2] / (2.0 * n2),
2305 ]
2306 };
2307 let axis = norm(n);
2308 let radius = dot(sub(a, centre), sub(a, centre)).sqrt();
2309 if !(radius > 1e-12) {
2310 return None;
2311 }
2312 let bar = radius * CIRCLE_FIT_TOLERANCE;
2313 for p in points {
2314 let d = sub(*p, centre);
2315 if (dot(d, d).sqrt() - radius).abs() > bar || dot(d, axis).abs() > bar {
2316 return None;
2317 }
2318 }
2319 let first = norm(sub(points[0], centre));
2322 let mut span = 0.0;
2323 for p in points {
2324 let d = norm(sub(*p, centre));
2325 let angle = dot(d, first).clamp(-1.0, 1.0).acos();
2326 span = f64::max(span, angle);
2327 }
2328 if span.to_degrees() < CIRCLE_MIN_SPAN_DEG {
2329 return None;
2330 }
2331 Some((centre, axis, radius))
2332}
2333
2334#[derive(Debug, Clone, Copy)]
2343struct Box2 {
2344 centre: [f64; 2],
2345 right: [f64; 2],
2346 up: [f64; 2],
2347 half: [f64; 2],
2348}
2349
2350impl Box2 {
2351 fn local(&self, p: [f64; 2]) -> Option<[f64; 2]> {
2355 let det = self.right[0] * self.up[1] - self.right[1] * self.up[0];
2356 if det.abs() < 1e-12 {
2357 return None;
2358 }
2359 let d = [p[0] - self.centre[0], p[1] - self.centre[1]];
2360 Some([
2361 (d[0] * self.up[1] - d[1] * self.up[0]) / det,
2362 (d[1] * self.right[0] - d[0] * self.right[1]) / det,
2363 ])
2364 }
2365
2366 fn of_text(run: &SheetText) -> Box2 {
2369 let rows = run.text.lines().count().max(1) as f64;
2370 Box2 {
2371 centre: run.anchor,
2372 right: run.right,
2373 up: run.up,
2374 half: [text_half_width(&run.text, 1.0), rows * 0.5 + TEXT_PAD],
2375 }
2376 }
2377}
2378
2379pub(super) fn text_half_width(text: &str, height: f64) -> f64 {
2384 let columns = text.lines().map(|line| line.chars().count()).max().unwrap_or(0) as f64;
2385 (columns * TEXT_ADVANCE * 0.5 + TEXT_PAD) * height
2386}
2387
2388fn break_for_text(lines: Vec<Vec<[f64; 2]>>, boxes: &[Box2]) -> Vec<Vec<[f64; 2]>> {
2401 if boxes.is_empty() {
2402 return lines;
2403 }
2404 let mut out: Vec<Vec<[f64; 2]>> = Vec::new();
2405 for line in lines {
2406 let mut current: Vec<[f64; 2]> = Vec::new();
2408 for pair in line.windows(2) {
2409 let (p, q) = (pair[0], pair[1]);
2410 let mut kept = visible_spans(p, q, boxes);
2411 if kept.is_empty() {
2412 if current.len() >= 2 {
2413 out.push(std::mem::take(&mut current));
2414 } else {
2415 current.clear();
2416 }
2417 continue;
2418 }
2419 let full = kept.len() == 1 && kept[0].0 <= 0.0 && kept[0].1 >= 1.0;
2420 let at = |t: f64| [p[0] + (q[0] - p[0]) * t, p[1] + (q[1] - p[1]) * t];
2421 if full {
2422 if current.is_empty() {
2423 current.push(p);
2424 }
2425 current.push(q);
2426 continue;
2427 }
2428 if !kept.is_empty() && kept[0].0 <= 0.0 {
2431 let (_, end) = kept.remove(0);
2432 if current.is_empty() {
2433 current.push(p);
2434 }
2435 current.push(at(end));
2436 }
2437 if current.len() >= 2 {
2438 out.push(std::mem::take(&mut current));
2439 } else {
2440 current.clear();
2441 }
2442 for (start, end) in kept {
2443 if end >= 1.0 {
2444 current = vec![at(start), q];
2445 } else {
2446 out.push(vec![at(start), at(end)]);
2447 }
2448 }
2449 }
2450 if current.len() >= 2 {
2451 out.push(current);
2452 }
2453 }
2454 out
2455}
2456
2457pub(super) fn break_for_text_boxes(
2461 lines: Vec<Vec<[f64; 2]>>,
2462 runs: &[SheetText],
2463) -> Vec<Vec<[f64; 2]>> {
2464 let boxes: Vec<Box2> = runs.iter().map(Box2::of_text).collect();
2465 break_for_text(lines, &boxes)
2466}
2467
2468fn visible_spans(p: [f64; 2], q: [f64; 2], boxes: &[Box2]) -> Vec<(f64, f64)> {
2471 let mut blocked: Vec<(f64, f64)> = Vec::new();
2472 for box2 in boxes {
2473 let (Some(a), Some(b)) = (box2.local(p), box2.local(q)) else {
2474 continue;
2475 };
2476 let (mut t0, mut t1) = (0.0f64, 1.0f64);
2478 let mut inside = true;
2479 for (axis, half) in [(0usize, box2.half[0]), (1usize, box2.half[1])] {
2480 let delta = b[axis] - a[axis];
2481 if delta.abs() < 1e-12 {
2482 if a[axis].abs() > half {
2483 inside = false;
2484 break;
2485 }
2486 continue;
2487 }
2488 let (lo, hi) = ((-half - a[axis]) / delta, (half - a[axis]) / delta);
2489 let (lo, hi) = if lo <= hi { (lo, hi) } else { (hi, lo) };
2490 t0 = t0.max(lo);
2491 t1 = t1.min(hi);
2492 if t0 > t1 {
2493 inside = false;
2494 break;
2495 }
2496 }
2497 if inside && t1 > t0 {
2498 blocked.push((t0, t1));
2499 }
2500 }
2501 if blocked.is_empty() {
2502 return vec![(0.0, 1.0)];
2503 }
2504 blocked.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
2505 let mut spans = Vec::new();
2506 let mut cursor = 0.0f64;
2507 for (start, end) in blocked {
2508 if start > cursor + 1e-9 {
2509 spans.push((cursor, start.min(1.0)));
2510 }
2511 cursor = cursor.max(end);
2512 if cursor >= 1.0 {
2513 break;
2514 }
2515 }
2516 if cursor < 1.0 - 1e-9 {
2517 spans.push((cursor, 1.0));
2518 }
2519 spans
2520}
2521
2522pub fn plane_is_edge_on(row: &PmiAnnotationReport, forward: [f64; 3]) -> bool {
2527 match row.plane.as_ref() {
2528 None => false,
2529 Some(plane) => dot(norm(plane.normal), forward).abs() < EDGE_ON_COS,
2530 }
2531}
2532
2533fn tolerance_sources(
2539 report: Option<&PmiViewReport>,
2540 view: &PmiView,
2541 frame: &ViewFrame,
2542 placed: &PlacedView,
2543 env: &brep_kernel::Env,
2544) -> Vec<ToleranceSource> {
2545 let Some(report) = report else { return Vec::new() };
2546 let project = |p: [f64; 3]| -> [f64; 2] {
2547 let v = frame.to_view(p);
2548 to_paper(placed, v[0], v[1])
2549 };
2550 let mut out = Vec::new();
2551 for row in report.annotations.iter().filter(|row| row.enabled && row.status == PmiStatus::Ok) {
2552 let Some(annotation) = view.annotations.iter().find(|annotation| annotation.id() == row.id) else { continue };
2553 let Ok(block) = brep_kernel::ToleranceBlock::read(annotation, env) else { continue };
2554 let reference = annotation.flag("isReference");
2555 if block.mode == brep_kernel::ToleranceMode::None && !reference {
2556 continue;
2557 }
2558 let measures = match &row.geometry {
2559 brep_kernel::PmiGeometry::Linear { a, b, component: None } => Measures::Linear { a: project(*a), b: project(*b) },
2560 brep_kernel::PmiGeometry::Radial { center, radius, diameter, sphere: false, .. } => {
2561 Measures::Radial { centre: project(*center), radius: *radius, diameter: *diameter }
2562 }
2563 _ => continue,
2564 };
2565 out.push(ToleranceSource { id: row.id.clone(), measures, block, reference });
2566 }
2567 out
2568}
2569
2570fn annotations(
2571 report: Option<&PmiViewReport>,
2572 view: &PmiView,
2573 frame: &ViewFrame,
2574 placed: &PlacedView,
2575) -> Vec<AnnotationDrawing> {
2576 let Some(report) = report else {
2577 return Vec::new();
2578 };
2579 let paper_text = view.display.text_size_pt * MM_PER_POINT;
2583 let text_height = paper_text / placed.scale.max(1e-9);
2584 let style = PmiLayoutStyle {
2585 arrow: text_height * ARROW_PER_TEXT,
2586 text_height,
2587 view_dir: frame.forward,
2588 view_up: frame.up,
2589 plane: None,
2590 };
2591 let flatten = placed.flatten_text;
2592 let mut out = Vec::new();
2593 for row in &report.annotations {
2594 if !row.enabled || row.status != PmiStatus::Ok {
2595 continue;
2596 }
2597 let row_style = PmiLayoutStyle { plane: row.plane, ..style };
2598 let drawn = pmi_present(&row.geometry, row.label_world, &row.text, &row_style);
2599 let project = |p: [f64; 3]| -> [f64; 2] {
2602 let v = frame.to_view(p);
2603 to_paper(placed, v[0], v[1])
2604 };
2605 let project_line = |polyline: &Vec<[f64; 3]>| -> Vec<[f64; 2]> {
2606 polyline.iter().map(|p| project(*p)).collect()
2607 };
2608 let mut lines: Vec<Vec<[f64; 2]>> = drawn.polylines.iter().map(project_line).collect();
2609 for arrow in &drawn.arrows {
2612 let mut closed: Vec<[f64; 3]> = arrow.to_vec();
2613 closed.push(arrow[0]);
2614 lines.push(project_line(&closed));
2615 }
2616
2617 let unit = drawn
2624 .texts
2625 .first()
2626 .filter(|_| !drawn.frames.is_empty())
2627 .map(|run| Unit {
2628 anchor: drawn.text_anchor,
2629 right: run.right,
2630 up: run.up,
2631 cap: run.height,
2632 });
2633 let flat_about = |anchor: [f64; 3], right3: [f64; 3], up3: [f64; 3], p: [f64; 3]| -> [f64; 2] {
2638 let d = sub(p, anchor);
2639 let a = project(anchor);
2640 [a[0] + dot(d, right3) * placed.scale, a[1] - dot(d, up3) * placed.scale]
2641 };
2642
2643 let texts: Vec<SheetText> = drawn
2644 .texts
2645 .iter()
2646 .filter(|run| !run.text.trim().is_empty())
2647 .map(|run| {
2648 let cap = run.height * placed.scale;
2649 if flatten {
2650 let anchor = match unit {
2654 Some(unit) => flat_about(unit.anchor, unit.right, unit.up, run.anchor),
2655 None => project(run.anchor),
2656 };
2657 return SheetText::flat(run.text.clone(), anchor, cap);
2658 }
2659 let image = |v: [f64; 3]| -> [f64; 2] {
2661 [dot(v, frame.right) * cap, -dot(v, frame.up) * cap]
2662 };
2663 let mut right = image(run.right);
2664 if dot(cross(run.right, run.up), frame.forward) > 0.0 {
2669 right = [-right[0], -right[1]];
2670 }
2671 SheetText::new(run.text.clone(), project(run.anchor), right, image(run.up))
2672 })
2673 .collect();
2674
2675 let frames: Vec<Vec<[f64; 2]>> = match (flatten, unit) {
2678 (true, Some(unit)) => drawn
2679 .frames
2680 .iter()
2681 .map(|polyline| {
2682 polyline
2683 .iter()
2684 .map(|p| flat_about(unit.anchor, unit.right, unit.up, *p))
2685 .collect()
2686 })
2687 .collect(),
2688 _ => drawn.frames.iter().map(project_line).collect(),
2689 };
2690 let mut boxes: Vec<Box2> = texts.iter().map(Box2::of_text).collect();
2694 if let Some(unit) = unit {
2695 let scaled = unit.cap * placed.scale;
2696 let (right, up) = if flatten {
2697 ([scaled, 0.0], [0.0, -scaled])
2698 } else {
2699 let image = |v: [f64; 3]| [dot(v, frame.right) * scaled, -dot(v, frame.up) * scaled];
2700 (image(unit.right), image(unit.up))
2701 };
2702 if let Some(box2) = unit.box_of(&drawn.frames, project(unit.anchor), right, up) {
2703 boxes.push(box2);
2704 }
2705 }
2706 let mut lines = break_for_text(lines, &boxes);
2707 lines.extend(frames);
2708
2709 out.push(AnnotationDrawing {
2710 id: row.id.clone(),
2711 kind: row.kind.clone(),
2712 text: row.text.clone(),
2713 lines,
2714 texts,
2715 note: if !plane_is_edge_on(row, frame.forward) {
2716 String::new()
2717 } else if flatten {
2718 format!(
2719 "annotation plane edge-on to '{}': its geometry projects to a line, its text is flattened onto the sheet",
2720 view.name
2721 )
2722 } else {
2723 format!(
2724 "annotation plane edge-on to '{}': it and its text project to a line",
2725 view.name
2726 )
2727 },
2728 });
2729 }
2730 out
2731}
2732
2733#[derive(Debug, Clone, Copy)]
2739struct Unit {
2740 anchor: [f64; 3],
2741 right: [f64; 3],
2742 up: [f64; 3],
2743 cap: f64,
2744}
2745
2746impl Unit {
2747 fn box_of(
2753 &self,
2754 frames: &[Vec<[f64; 3]>],
2755 anchor: [f64; 2],
2756 right: [f64; 2],
2757 up: [f64; 2],
2758 ) -> Option<Box2> {
2759 if self.cap <= 0.0 {
2760 return None;
2761 }
2762 let mut lo = [f64::INFINITY; 2];
2763 let mut hi = [f64::NEG_INFINITY; 2];
2764 for polyline in frames {
2765 for p in polyline {
2766 let d = sub(*p, self.anchor);
2767 let local = [dot(d, self.right) / self.cap, dot(d, self.up) / self.cap];
2768 for axis in 0..2 {
2769 lo[axis] = lo[axis].min(local[axis]);
2770 hi[axis] = hi[axis].max(local[axis]);
2771 }
2772 }
2773 }
2774 if lo[0] > hi[0] {
2775 return None;
2776 }
2777 let mid = [(lo[0] + hi[0]) * 0.5, (lo[1] + hi[1]) * 0.5];
2778 Some(Box2 {
2779 centre: [
2780 anchor[0] + mid[0] * right[0] + mid[1] * up[0],
2781 anchor[1] + mid[0] * right[1] + mid[1] * up[1],
2782 ],
2783 right,
2784 up,
2785 half: [(hi[0] - lo[0]) * 0.5, (hi[1] - lo[1]) * 0.5],
2786 })
2787 }
2788}
2789
2790
2791