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 #[serde(skip_serializing_if = "Vec::is_empty")]
148 pub solids: Vec<String>,
149 pub edges: Vec<Vec<[f64; 2]>>,
151 #[serde(skip_serializing_if = "Vec::is_empty")]
155 pub highlighted: Vec<Vec<[f64; 2]>>,
156 #[serde(skip_serializing_if = "Option::is_none")]
161 pub line_art: Option<LineArtLayer>,
162 pub annotations: Vec<AnnotationDrawing>,
164 pub anchors: Vec<AnchorPoint>,
170 #[serde(rename = "textMm")]
174 pub text_mm: f64,
175 #[serde(skip_serializing_if = "Option::is_none")]
178 pub section: Option<SectionDrawing>,
179 #[serde(rename = "sectionMarks", skip_serializing_if = "Vec::is_empty")]
182 pub section_marks: Vec<SectionMark>,
183 #[serde(skip_serializing_if = "Option::is_none")]
186 pub detail: Option<DetailDrawing>,
187 #[serde(rename = "detailMarks", skip_serializing_if = "Vec::is_empty")]
190 pub detail_marks: Vec<DetailMark>,
191 pub bounds: [f64; 4],
194 #[serde(rename = "hiddenLine", skip_serializing_if = "String::is_empty")]
200 pub hidden_line: String,
201 #[serde(skip)]
207 pub tolerances: Vec<ToleranceSource>,
208}
209
210#[derive(Debug, Clone, PartialEq)]
213pub struct ToleranceSource {
214 pub id: String,
216 pub measures: Measures,
217 pub block: brep_kernel::ToleranceBlock,
218 pub reference: bool,
219}
220
221#[derive(Debug, Clone, PartialEq)]
223pub enum Measures {
224 Linear { a: [f64; 2], b: [f64; 2] },
228 Radial { centre: [f64; 2], radius: f64, diameter: bool },
231}
232
233impl ViewDrawing {
234 fn empty(placed: &PlacedView, name: String) -> ViewDrawing {
238 ViewDrawing {
239 id: placed.id.clone(),
240 view: placed.view.clone(),
241 name,
242 position: placed.position,
243 scale: placed.scale,
244 error: String::new(),
245 solids: Vec::new(),
246 edges: Vec::new(),
247 highlighted: Vec::new(),
248 line_art: None,
249 annotations: Vec::new(),
250 anchors: Vec::new(),
251 text_mm: 0.0,
252 section: None,
253 section_marks: Vec::new(),
254 detail: None,
255 detail_marks: Vec::new(),
256 bounds: [placed.position[0], placed.position[1], placed.position[0], placed.position[1]],
257 hidden_line: String::new(),
258 tolerances: Vec::new(),
259 }
260 }
261}
262
263#[derive(Debug, Clone, PartialEq, serde::Serialize)]
270pub struct LineArtLayer {
271 pub ground: [f64; 4],
273 pub fills: Vec<([[f64; 2]; 3], bool)>,
275 pub outlines: Vec<(Vec<[f64; 2]>, bool)>,
277 pub arrows: Vec<([f64; 2], [f64; 2])>,
279}
280
281impl LineArtLayer {
282 pub const GROUND: [f64; 3] = [0.925, 0.925, 0.925];
284 pub const PLACED_FILL: [f64; 3] = [1.0, 1.0, 1.0];
285 pub const ADDING_FILL: [f64; 3] = [0.616, 0.722, 0.961];
286 pub const PLACED_EDGE: [f64; 3] = [0.55, 0.55, 0.55];
287 pub const PLACED_OUTLINE: [f64; 3] = [0.30, 0.30, 0.30];
288 pub const INK: [f64; 3] = [0.0, 0.0, 0.0];
289 pub const PLACED_EDGE_MM: f64 = 0.18;
291 pub const ADDING_EDGE_MM: f64 = 0.22;
292 pub const PLACED_OUTLINE_MM: f64 = 0.35;
293 pub const ADDING_OUTLINE_MM: f64 = 0.6;
294 pub const SEAM_MM: f64 = 0.15;
296 pub const ARROW_SHAFT_MM: f64 = 0.4;
297 pub const ARROW_HEAD_MM: f64 = 3.6;
298 pub const ARROW_HEAD_WIDTH_MM: f64 = 2.6;
299
300 pub fn head(tail: [f64; 2], tip: [f64; 2]) -> Option<([f64; 2], [[f64; 2]; 3])> {
302 let (dx, dy) = (tip[0] - tail[0], tip[1] - tail[1]);
303 let length = (dx * dx + dy * dy).sqrt();
304 if !(length > 1e-6) {
305 return None;
306 }
307 let (ux, uy) = (dx / length, dy / length);
308 let head = Self::ARROW_HEAD_MM.min(length * 0.6);
309 let base = [tip[0] - ux * head, tip[1] - uy * head];
310 let half = Self::ARROW_HEAD_WIDTH_MM * 0.5 * head / Self::ARROW_HEAD_MM;
311 Some((base, [tip, [base[0] - uy * half, base[1] + ux * half], [base[0] + uy * half, base[1] - ux * half]]))
312 }
313}
314
315#[derive(Debug, Clone, PartialEq, serde::Serialize)]
321pub struct DetailDrawing {
322 pub label: String,
323 pub centre: [f64; 2],
325 #[serde(rename = "radiusMm")]
328 pub radius_mm: f64,
329 pub circle: Vec<[f64; 2]>,
331 pub texts: Vec<SheetText>,
333}
334
335#[derive(Debug, Clone, PartialEq, serde::Serialize)]
338pub struct DetailMark {
339 pub id: String,
341 pub label: String,
342 pub centre: [f64; 2],
343 #[serde(rename = "radiusMm")]
344 pub radius_mm: f64,
345 pub lines: Vec<Vec<[f64; 2]>>,
346 pub texts: Vec<SheetText>,
347}
348
349#[derive(Debug, Clone, PartialEq, serde::Serialize)]
357pub struct SectionDrawing {
358 pub label: String,
360 pub hatch: Vec<Vec<[f64; 2]>>,
363 pub cut: Vec<Vec<[f64; 2]>>,
370 pub texts: Vec<SheetText>,
372}
373
374#[derive(Debug, Clone, PartialEq, serde::Serialize)]
378pub struct SectionMark {
379 pub id: String,
381 pub label: String,
382 pub lines: Vec<Vec<[f64; 2]>>,
383 pub texts: Vec<SheetText>,
384}
385
386#[derive(Debug, Clone, PartialEq, serde::Serialize)]
388pub struct AnnotationDrawing {
389 pub id: String,
390 #[serde(rename = "type")]
391 pub kind: String,
392 pub text: String,
395 pub lines: Vec<Vec<[f64; 2]>>,
397 pub texts: Vec<SheetText>,
398 #[serde(skip_serializing_if = "String::is_empty")]
404 pub note: String,
405}
406
407#[derive(Debug, Clone, PartialEq, serde::Serialize)]
416pub struct AnchorPoint {
417 #[serde(rename = "ref")]
418 pub anchor: String,
419 pub kind: String,
421 pub at: [f64; 2],
423 pub model: [f64; 3],
427 #[serde(skip_serializing_if = "Option::is_none")]
438 pub dir: Option<[f64; 2]>,
439 #[serde(skip_serializing_if = "Option::is_none")]
441 pub radius: Option<f64>,
442 #[serde(skip_serializing_if = "Option::is_none")]
446 pub u: Option<[f64; 2]>,
447 #[serde(skip_serializing_if = "Option::is_none")]
448 pub v: Option<[f64; 2]>,
449}
450
451#[derive(Debug, Clone, PartialEq, serde::Serialize)]
469pub struct SheetText {
470 pub text: String,
471 pub anchor: [f64; 2],
472 pub height: f64,
473 pub angle: f64,
474 pub right: [f64; 2],
475 pub up: [f64; 2],
476}
477
478impl SheetText {
479 pub fn new(text: String, anchor: [f64; 2], right: [f64; 2], up: [f64; 2]) -> SheetText {
482 SheetText {
483 text,
484 anchor,
485 height: (up[0] * up[0] + up[1] * up[1]).sqrt(),
486 angle: right[1].atan2(right[0]).to_degrees(),
489 right,
490 up,
491 }
492 }
493
494 pub fn flat(text: String, anchor: [f64; 2], height: f64) -> SheetText {
498 SheetText::new(text, anchor, [height, 0.0], [0.0, -height])
499 }
500}
501
502#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
510pub struct ViewFrame {
511 pub target: [f64; 3],
512 pub right: [f64; 3],
513 pub up: [f64; 3],
514 pub forward: [f64; 3],
515}
516
517use crate::geometry3d::sub3 as sub;
518
519use crate::geometry3d::cross3 as cross;
520
521use crate::geometry3d::dot3 as dot;
522
523fn norm(v: [f64; 3]) -> [f64; 3] {
524 let len = dot(v, v).sqrt();
525 if len < 1e-12 {
526 [0.0, 0.0, 0.0]
527 } else {
528 [v[0] / len, v[1] / len, v[2] / len]
529 }
530}
531
532impl ViewFrame {
533 pub fn of(camera: &PmiCamera) -> ViewFrame {
537 let forward = norm(sub(camera.target, camera.eye));
538 let forward = if forward == [0.0, 0.0, 0.0] { [0.0, 0.0, -1.0] } else { forward };
539 let mut right = norm(cross(forward, camera.up));
540 if right == [0.0, 0.0, 0.0] {
541 right = norm(cross(forward, [0.0, 0.0, 1.0]));
544 if right == [0.0, 0.0, 0.0] {
545 right = norm(cross(forward, [0.0, 1.0, 0.0]));
546 }
547 }
548 let up = cross(right, forward);
549 ViewFrame { target: camera.target, right, up, forward }
550 }
551
552 pub fn to_view(&self, p: [f64; 3]) -> [f64; 3] {
555 let d = sub(p, self.target);
556 [dot(d, self.right), dot(d, self.up), dot(d, self.forward)]
557 }
558}
559
560pub(crate) fn to_paper(placed: &PlacedView, x: f64, y: f64) -> [f64; 2] {
562 [placed.position[0] + x * placed.scale, placed.position[1] - y * placed.scale]
563}
564
565pub fn project_view(
573 scene: &RenderScene,
574 placed: &PlacedView,
575 view: Option<&PmiView>,
576 report: Option<&PmiViewReport>,
577) -> ViewDrawing {
578 let (unposed, env) = (Unposed::new(), brep_kernel::Env::default());
579 project_view_through(&Model { scene, unposed: &unposed, env: &env }, placed, view, report, None, None, &mut LinesMode::Compute)
580}
581
582pub struct Model<'a> {
588 pub scene: &'a RenderScene,
589 pub unposed: &'a Unposed,
590 pub env: &'a brep_kernel::Env,
591}
592
593pub type Unposed = std::collections::HashMap<String, crate::scene::SolidDisplay>;
597
598#[derive(Debug, Clone, Copy, PartialEq, Eq)]
600pub enum HiddenLine {
601 Analytic,
603 Mesh,
606}
607
608impl HiddenLine {
609 pub fn label(self) -> &'static str {
610 match self {
611 HiddenLine::Analytic => "analytic",
612 HiddenLine::Mesh => "mesh",
613 }
614 }
615}
616
617pub struct SectionSetup {
621 pub frame: ViewFrame,
622 pub plane: ClipPlane,
623 pub half: Option<super::hlr::HalfLine>,
624 pub label: String,
625}
626
627pub struct DetailSetup {
632 pub frame: ViewFrame,
633 pub radius: f64,
634 pub label: String,
635}
636
637pub enum Derived {
640 Section(SectionSetup),
641 Detail(DetailSetup),
642}
643
644pub fn project_view_through(
665 model: &Model,
666 placed: &PlacedView,
667 view: Option<&PmiView>,
668 report: Option<&PmiViewReport>,
669 derived: Option<Derived>,
670 letter: Option<&str>,
671 mode: &mut LinesMode,
672) -> ViewDrawing {
673 let (scene, unposed) = (model.scene, model.unposed);
674 let mut out = ViewDrawing::empty(placed, view.map(|v| v.name.clone()).unwrap_or_default());
675 let Some(view) = view else {
676 out.error = format!("no PMI view '{}'", placed.view);
677 return out;
678 };
679 let Some(camera) = view.camera.as_ref() else {
680 out.error = format!("PMI view '{}' has no captured camera", placed.view);
681 return out;
682 };
683 if !matches!(camera.projection, PmiProjection::Orthographic { .. }) {
684 out.error = format!(
685 "PMI view '{}' is a perspective camera; a sheet draws {ORTHOGRAPHIC} views only",
686 placed.view
687 );
688 return out;
689 }
690 if !placed.projection.eq_ignore_ascii_case(ORTHOGRAPHIC) {
691 out.error = format!("projection '{}' is not drawn on a sheet", placed.projection);
692 return out;
693 }
694
695 let saved_section = derived.is_none() && view.display.section.is_some();
696 let planes = if derived.is_none() { view.section_planes_in(report) } else { Vec::new() };
699 if planes.len() > 2 || planes.get(1).is_some_and(|(_, second)| dot(*second, planes[0].1).abs() > 1e-6) {
700 out.error = format!(
701 "PMI view '{}' is sectioned by {} planes; a sheet draws one plane, or a half section of two perpendicular planes",
702 placed.view,
703 planes.len()
704 );
705 return out;
706 }
707 let derived = derived.or_else(|| planes.first().map(|&(point, normal)| {
708 let original = ViewFrame::of(camera);
709 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.] };
710 let right = norm(cross(normal, seed));
711 let frame = ViewFrame { target: point, right, up: cross(right, normal), forward: normal };
712 let half = planes.get(1).map(|&(at, second)| {
713 let (normal, at) = ([dot(second, frame.right), dot(second, frame.up)], frame.to_view(at));
714 super::hlr::HalfLine { normal, offset: normal[0] * at[0] + normal[1] * at[1] }
715 });
716 Derived::Section(SectionSetup { frame, plane: ClipPlane { point, normal }, half, label: letter.unwrap_or("A").to_string() })
717 }));
718 let frame = match &derived {
719 Some(Derived::Section(setup)) => setup.frame,
720 Some(Derived::Detail(setup)) => setup.frame,
721 None => ViewFrame::of(camera),
722 };
723 out.text_mm = view.display.text_size_pt * MM_PER_POINT;
724 out.solids = drawn_solids(scene, unposed, view).iter().map(|solid| solid.name.clone()).collect();
725 let cap = if out.text_mm > 1e-9 { out.text_mm } else { 12.0 * MM_PER_POINT };
726 let plan = exact_solids(scene, unposed, view, report);
731 let clip = match &derived {
732 None => LinesClip::Whole,
733 Some(Derived::Detail(setup)) => LinesClip::Circle { radius: setup.radius },
734 Some(Derived::Section(setup)) => LinesClip::Section { half: setup.half },
735 };
736 let exact = plan.as_ref().and_then(|plan| exact_lines_for(plan, &frame, placed, clip, mode));
737 if matches!(mode, LinesMode::Export(_)) && exact.is_none() {
738 if plan.is_none() {
739 out.error = "exact geometry is unavailable for this picture".into();
740 }
741 return out;
742 }
743 out.hidden_line = if exact.is_some() { HiddenLine::Analytic } else { HiddenLine::Mesh }.label().into();
744 match &derived {
745 None => {
746 out.edges = match exact {
747 Some(lines) => lines.runs,
748 None => mesh_lines_for(scene, unposed, view, report, &frame, placed, LinesClip::Whole, None).runs,
749 };
750 out.annotations = annotations(report, view, &frame, placed);
751 out.tolerances = tolerance_sources(report, view, &frame, placed, model.env);
752 }
753 Some(Derived::Detail(setup)) => {
754 let radius_mm = setup.radius * placed.scale;
755 let lines = match exact {
759 Some(lines) => lines.runs,
760 None => mesh_lines_for(scene, unposed, view, report, &frame, placed, clip, None).runs,
761 };
762 out.edges = clip_to_circle(lines, placed.position, radius_mm);
763 out.detail = Some(DetailDrawing {
764 label: setup.label.clone(),
765 centre: placed.position,
766 radius_mm,
767 circle: circle_polyline(placed.position, radius_mm),
768 texts: Vec::new(),
769 });
770 }
771 Some(Derived::Section(setup)) => match exact {
772 Some(ExactLines { runs, cut }) => {
773 let mut edges = runs;
776 edges.extend(cut.iter().cloned());
777 out.edges = edges;
778 out.section = Some(SectionDrawing {
779 label: setup.label.clone(),
780 hatch: hatch(&segments_of(&cut)),
781 cut,
782 texts: Vec::new(),
783 });
784 }
785 None => {
786 let ExactLines { runs, cut: cut_paper } =
787 mesh_lines_for(scene, unposed, view, report, &frame, placed, clip, Some(&setup.plane));
788 let mut edges = runs;
789 edges.extend(cut_paper.iter().cloned());
790 out.edges = edges;
791 out.section = Some(SectionDrawing {
792 label: setup.label.clone(),
793 hatch: hatch(&cut_paper),
794 cut: cut_paper,
795 texts: Vec::new(),
796 });
797 }
798 },
799 }
800 if saved_section {
801 out.annotations = annotations(report, view, &frame, placed);
802 out.tolerances = tolerance_sources(report, view, &frame, placed, model.env);
803 }
804 out.anchors = anchor_points(scene, unposed, view, report, &frame, placed);
805 if let Some(detail) = &out.detail {
812 let reach = detail.radius_mm * (1.0 + 1e-9) + 1e-9;
813 out.anchors.retain(|anchor| {
814 let (dx, dy) = (anchor.at[0] - detail.centre[0], anchor.at[1] - detail.centre[1]);
815 (dx * dx + dy * dy).sqrt() <= reach
816 });
817 }
818 out.bounds = bounds_of(&out, placed);
819 let middle = (out.bounds[0] + out.bounds[2]) * 0.5;
823 let below = out.bounds[3] + cap * 2.0;
824 if let Some(section) = out.section.as_mut() {
825 section.texts.push(SheetText::flat(
826 format!("SECTION {}\u{2014}{}", section.label, section.label),
827 [middle, below],
828 cap,
829 ));
830 out.bounds = bounds_of(&out, placed);
831 }
832 if let Some(detail) = out.detail.as_mut() {
835 detail.texts.push(SheetText::flat(format!("DETAIL {}", detail.label), [middle, below], cap));
836 detail.texts.push(SheetText::flat(
837 format!("SCALE {}", super::frame::ratio_text(placed.scale)),
838 [middle, below + cap * 1.8],
839 cap * 0.8,
840 ));
841 out.bounds = bounds_of(&out, placed);
842 }
843 out
844}
845
846pub fn project_sheet(
852 scene: &RenderScene,
853 sheet: &super::Sheet,
854 views: &[PmiView],
855 report: Option<&brep_kernel::PmiReport>,
856 context: &super::frame::FrameContext,
857) -> SheetDrawing {
858 let (unposed, env) = (Unposed::new(), brep_kernel::Env::default());
859 project_sheet_with(&Model { scene, unposed: &unposed, env: &env }, sheet, views, report, context, &mut LinesMode::Compute)
860}
861
862pub fn project_sheet_with(
866 model: &Model,
867 sheet: &super::Sheet,
868 views: &[PmiView],
869 report: Option<&brep_kernel::PmiReport>,
870 context: &super::frame::FrameContext,
871 mode: &mut LinesMode,
872) -> SheetDrawing {
873 let (width_mm, height_mm) = sheet.millimetres();
874 let letters = sheet.view_letters(views);
882 let mut drawn: Vec<ViewDrawing> = sheet
883 .views
884 .iter()
885 .zip(&letters)
886 .map(|(placed, letter)| {
887 let saved = views.iter().find(|view| view.id == placed.view);
888 if placed.section.is_some() || placed.detail.is_some() {
889 ViewDrawing::empty(placed, saved.map(|v| v.name.clone()).unwrap_or_default())
893 } else {
894 project_view_through(model, placed, saved, report.and_then(|report| report.view(&placed.view)), None, letter.as_deref(), mode)
895 }
896 })
897 .collect();
898 for (index, placed) in sheet.views.iter().enumerate() {
899 let Some(circle) = placed.detail.as_ref() else { continue };
900 match detail_setup(sheet, &drawn, circle, views) {
901 Err(error) => drawn[index].error = error,
902 Ok((setup, source, mark)) => {
903 drawn[index] = project_view_through(
904 model,
905 placed,
906 views.iter().find(|view| view.id == placed.view),
907 report.and_then(|report| report.view(&placed.view)),
908 Some(Derived::Detail(setup)),
909 None,
910 mode,
911 );
912 let mark = DetailMark { id: placed.id.clone(), ..mark };
913 drawn[source].detail_marks.push(mark);
914 let source_placed = &sheet.views[source];
915 drawn[source].bounds = bounds_of(&drawn[source], source_placed);
916 }
917 }
918 }
919 for (index, placed) in sheet.views.iter().enumerate() {
920 let Some(cut) = placed.section.as_ref() else { continue };
921 match section_setup(sheet, &drawn, placed, cut, views) {
922 Err(error) => drawn[index].error = error,
923 Ok((setup, source, mark)) => {
924 drawn[index] = project_view_through(
925 model,
926 placed,
927 views.iter().find(|view| view.id == placed.view),
928 report.and_then(|report| report.view(&placed.view)),
929 Some(Derived::Section(setup)),
930 None,
931 mode,
932 );
933 drawn[source].section_marks.push(mark);
937 let source_placed = &sheet.views[source];
938 drawn[source].bounds = bounds_of(&drawn[source], source_placed);
939 }
940 }
941 }
942 for (index, placed) in sheet.views.iter().enumerate() {
945 if placed.section.is_some() || placed.detail.is_some() {
946 continue;
947 }
948 let Some(label) = drawn[index].section.as_ref().map(|section| section.label.clone()) else { continue };
949 let Some(view) = views.iter().find(|view| view.id == placed.view) else { continue };
950 let Some((point, normal)) = view.section_plane_in(report.and_then(|report| report.view(&placed.view))) else { continue };
951 for (other, source) in sheet.views.iter().enumerate() {
952 if other == index || source.section.is_some() || source.detail.is_some() {
953 continue;
954 }
955 if drawn[other].section.is_some() || !drawn[other].error.is_empty() {
956 continue;
957 }
958 let Some(camera) = views.iter().find(|view| view.id == source.view).and_then(|view| view.camera.as_ref()) else { continue };
959 let frame = ViewFrame::of(camera);
960 if dot(normal, frame.forward).abs() > 1e-6 {
961 continue;
962 }
963 let Some(mark) = plane_mark(&drawn[other], source, &frame, point, normal, &placed.id, &label) else { continue };
964 drawn[other].section_marks.push(mark);
965 drawn[other].bounds = bounds_of(&drawn[other], source);
966 }
967 }
968 let dimensions = super::dimension::layout_dimensions(
972 &sheet.dimensions,
973 &drawn,
974 [width_mm * 0.5, height_mm * 0.5],
975 );
976 let ordinates = super::ordinate::layout_ordinates(
977 &sheet.ordinates,
978 &drawn,
979 [width_mm * 0.5, height_mm * 0.5],
980 );
981 SheetDrawing {
982 id: sheet.id.clone(),
983 name: sheet.name.clone(),
984 width_mm,
985 height_mm,
986 frame: super::frame::frame(sheet, context),
987 views: drawn,
988 dimensions,
989 ordinates,
990 }
991}
992
993fn section_setup(
1020 sheet: &super::Sheet,
1021 drawn: &[ViewDrawing],
1022 placed: &PlacedView,
1023 cut: &super::SectionCut,
1024 views: &[PmiView],
1025) -> Result<(SectionSetup, usize, SectionMark), String> {
1026 if cut.from.is_empty() || cut.to.is_empty() {
1029 return Err("no cutting line yet \u{2014} pick its two anchors on a placed view".into());
1030 }
1031 let Some(source_index) = sheet.views.iter().position(|v| v.id == cut.source) else {
1032 return Err(format!("no placement '{}' to cut through on this sheet", cut.source));
1033 };
1034 let source_placed = &sheet.views[source_index];
1035 if source_placed.section.is_some() {
1036 return Err(format!(
1037 "'{}' is itself a section; a section of a section is not drawn",
1038 cut.source
1039 ));
1040 }
1041 if source_placed.detail.is_some() {
1042 return Err(format!(
1043 "'{}' is a detail; a section of a detail is not drawn",
1044 cut.source
1045 ));
1046 }
1047 let source = &drawn[source_index];
1048 if !source.error.is_empty() {
1049 return Err(format!("placement '{}' did not project", cut.source));
1050 }
1051 let (from, to) = (point_of(source, &cut.from)?, point_of(source, &cut.to)?);
1052 let span = [to[0] - from[0], to[1] - from[1]];
1053 let length = (span[0] * span[0] + span[1] * span[1]).sqrt();
1054 if !(length > 1e-9) {
1055 return Err(format!(
1056 "'{}' and '{}' are the same point; a cutting line needs two",
1057 cut.from, cut.to
1058 ));
1059 }
1060 let Some(camera) = views
1061 .iter()
1062 .find(|view| view.id == source_placed.view)
1063 .and_then(|view| view.camera.as_ref())
1064 else {
1065 return Err(format!("placement '{}' has no captured camera to cut in", cut.source));
1066 };
1067 let source_frame = ViewFrame::of(camera);
1068 if source_placed.scale <= 0.0 {
1069 return Err(format!("placement '{}' has no scale", cut.source));
1070 }
1071 let (p0, p1) = (lift(&source_frame, source_placed, from), lift(&source_frame, source_placed, to));
1076 let along = norm(sub(p1, p0));
1077 let mut forward = norm(cross(source_frame.forward, along));
1078 if forward == [0.0, 0.0, 0.0] {
1079 return Err("the cutting line has no direction in this view".into());
1080 }
1081 if cut.flip {
1082 forward = [-forward[0], -forward[1], -forward[2]];
1083 }
1084 let up = [-source_frame.forward[0], -source_frame.forward[1], -source_frame.forward[2]];
1085 let right = norm(cross(forward, up));
1086 let target = [(p0[0] + p1[0]) * 0.5, (p0[1] + p1[1]) * 0.5, (p0[2] + p1[2]) * 0.5];
1087 let setup = SectionSetup {
1088 frame: ViewFrame { target, right, up: cross(right, forward), forward },
1089 plane: ClipPlane { point: target, normal: forward },
1090 half: None,
1091 label: cut.label.clone(),
1092 };
1093 let cap = if source.text_mm > 1e-9 { source.text_mm } else { 12.0 * MM_PER_POINT };
1094 Ok((setup, source_index, section_mark(placed, cut, from, to, cap)))
1095}
1096
1097fn section_mark(
1101 placed: &PlacedView,
1102 cut: &super::SectionCut,
1103 from: [f64; 2],
1104 to: [f64; 2],
1105 cap: f64,
1106) -> SectionMark {
1107 let span = [to[0] - from[0], to[1] - from[1]];
1108 let length = (span[0] * span[0] + span[1] * span[1]).sqrt().max(1e-9);
1109 let along = [span[0] / length, span[1] / length];
1110 let sign = if cut.flip { -1.0 } else { 1.0 };
1115 let look = [-along[1] * sign, along[0] * sign];
1116 cutting_mark(&placed.id, &cut.label, from, to, look, cap)
1117}
1118
1119fn plane_mark(
1124 source: &ViewDrawing,
1125 placed: &PlacedView,
1126 frame: &ViewFrame,
1127 point: [f64; 3],
1128 normal: [f64; 3],
1129 id: &str,
1130 label: &str,
1131) -> Option<SectionMark> {
1132 let paper = |v: [f64; 3]| {
1133 let length = (v[0] * v[0] + v[1] * v[1]).sqrt();
1134 (length > 1e-9).then(|| [v[0] / length, v[1] / length])
1135 };
1136 let at = frame.to_view(point);
1138 let centre = to_paper(placed, at[0], at[1]);
1139 let trace = cross(normal, frame.forward);
1140 let along = paper([dot(trace, frame.right), -dot(trace, frame.up), 0.0])?;
1141 let look = paper([dot(normal, frame.right), -dot(normal, frame.up), 0.0])?;
1142 let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
1143 for p in source.edges.iter().flatten() {
1144 let t = (p[0] - centre[0]) * along[0] + (p[1] - centre[1]) * along[1];
1145 lo = lo.min(t);
1146 hi = hi.max(t);
1147 }
1148 if !(hi > lo) {
1149 return None;
1150 }
1151 let from = [centre[0] + along[0] * lo, centre[1] + along[1] * lo];
1152 let to = [centre[0] + along[0] * hi, centre[1] + along[1] * hi];
1153 let cap = if source.text_mm > 1e-9 { source.text_mm } else { 12.0 * MM_PER_POINT };
1154 Some(cutting_mark(id, label, from, to, look, cap))
1155}
1156
1157fn cutting_mark(id: &str, label: &str, from: [f64; 2], to: [f64; 2], look: [f64; 2], cap: f64) -> SectionMark {
1161 let span = [to[0] - from[0], to[1] - from[1]];
1162 let length = (span[0] * span[0] + span[1] * span[1]).sqrt().max(1e-9);
1163 let along = [span[0] / length, span[1] / length];
1164 let tail = cap * 1.5;
1165 let arrow = cap * 1.2;
1166 let mut lines = vec![vec![
1167 [from[0] - along[0] * tail, from[1] - along[1] * tail],
1168 [to[0] + along[0] * tail, to[1] + along[1] * tail],
1169 ]];
1170 let mut texts = Vec::new();
1171 for (end, outward) in [(from, [-along[0], -along[1]]), (to, along)] {
1172 let root = [end[0] + outward[0] * tail, end[1] + outward[1] * tail];
1173 let tip = [root[0] + look[0] * arrow * 2.0, root[1] + look[1] * arrow * 2.0];
1174 lines.push(vec![root, tip]);
1178 let across = [-look[1] * arrow * 0.35, look[0] * arrow * 0.35];
1179 lines.push(vec![
1180 tip,
1181 [tip[0] - look[0] * arrow + across[0], tip[1] - look[1] * arrow + across[1]],
1182 [tip[0] - look[0] * arrow - across[0], tip[1] - look[1] * arrow - across[1]],
1183 tip,
1184 ]);
1185 texts.push(SheetText::flat(
1186 label.to_string(),
1187 [
1188 root[0] + outward[0] * cap * 1.2 + look[0] * cap * 0.6,
1189 root[1] + outward[1] * cap * 1.2 + look[1] * cap * 0.6,
1190 ],
1191 cap,
1192 ));
1193 }
1194 SectionMark { id: id.to_string(), label: label.to_string(), lines, texts }
1195}
1196
1197fn point_of(source: &ViewDrawing, reference: &str) -> Result<[f64; 2], String> {
1200 source
1201 .anchors
1202 .iter()
1203 .find(|a| a.anchor == reference)
1204 .map(|a| a.at)
1205 .ok_or_else(|| format!("'{reference}' is not a point of '{}' any more", source.id))
1206}
1207
1208fn lift(frame: &ViewFrame, placed: &PlacedView, p: [f64; 2]) -> [f64; 3] {
1211 let x = (p[0] - placed.position[0]) / placed.scale;
1212 let y = -(p[1] - placed.position[1]) / placed.scale;
1213 [
1214 frame.target[0] + frame.right[0] * x + frame.up[0] * y,
1215 frame.target[1] + frame.right[1] * x + frame.up[1] * y,
1216 frame.target[2] + frame.right[2] * x + frame.up[2] * y,
1217 ]
1218}
1219
1220fn detail_setup(
1242 sheet: &super::Sheet,
1243 drawn: &[ViewDrawing],
1244 circle: &super::DetailCircle,
1245 views: &[PmiView],
1246) -> Result<(DetailSetup, usize, DetailMark), String> {
1247 if circle.centre.is_empty() || circle.rim.is_empty() {
1250 return Err("no detail circle yet \u{2014} pick its centre and a point on its rim on a placed view".into());
1251 }
1252 let Some(source_index) = sheet.views.iter().position(|v| v.id == circle.source) else {
1253 return Err(format!("no placement '{}' to draw a detail of on this sheet", circle.source));
1254 };
1255 let source_placed = &sheet.views[source_index];
1256 if source_placed.detail.is_some() {
1257 return Err(format!(
1258 "'{}' is itself a detail; a detail of a detail is not drawn",
1259 circle.source
1260 ));
1261 }
1262 if source_placed.section.is_some() {
1263 return Err(format!(
1264 "'{}' is a section; a detail of a section is not drawn",
1265 circle.source
1266 ));
1267 }
1268 let source = &drawn[source_index];
1269 if !source.error.is_empty() {
1270 return Err(format!("placement '{}' did not project", circle.source));
1271 }
1272 let (centre, rim) = (point_of(source, &circle.centre)?, point_of(source, &circle.rim)?);
1273 let radius_mm = ((rim[0] - centre[0]).powi(2) + (rim[1] - centre[1]).powi(2)).sqrt();
1274 if !(radius_mm > 1e-9) {
1275 return Err(format!(
1276 "'{}' and '{}' land on the same point of '{}'; a detail circle needs a radius",
1277 circle.centre, circle.rim, circle.source
1278 ));
1279 }
1280 let Some(camera) = views
1281 .iter()
1282 .find(|view| view.id == source_placed.view)
1283 .and_then(|view| view.camera.as_ref())
1284 else {
1285 return Err(format!("placement '{}' has no captured camera to draw a detail of", circle.source));
1286 };
1287 if source_placed.scale <= 0.0 {
1288 return Err(format!("placement '{}' has no scale", circle.source));
1289 }
1290 let source_frame = ViewFrame::of(camera);
1291 let setup = DetailSetup {
1292 frame: ViewFrame { target: lift(&source_frame, source_placed, centre), ..source_frame },
1293 radius: radius_mm / source_placed.scale,
1294 label: circle.label.clone(),
1295 };
1296 let cap = if source.text_mm > 1e-9 { source.text_mm } else { 12.0 * MM_PER_POINT };
1297 let d = std::f64::consts::FRAC_1_SQRT_2;
1301 let reach = radius_mm + cap * 1.5;
1302 let mark = DetailMark {
1303 id: String::new(),
1304 label: circle.label.clone(),
1305 centre,
1306 radius_mm,
1307 lines: vec![circle_polyline(centre, radius_mm)],
1308 texts: vec![SheetText::flat(
1309 circle.label.clone(),
1310 [centre[0] + reach * d, centre[1] - reach * d],
1311 cap,
1312 )],
1313 };
1314 Ok((setup, source_index, mark))
1315}
1316
1317const CIRCLE_SEGMENTS: usize = 96;
1321
1322fn circle_polyline(centre: [f64; 2], radius: f64) -> Vec<[f64; 2]> {
1324 (0..=CIRCLE_SEGMENTS)
1325 .map(|k| {
1326 let t = std::f64::consts::TAU * k as f64 / CIRCLE_SEGMENTS as f64;
1327 [centre[0] + radius * t.cos(), centre[1] + radius * t.sin()]
1328 })
1329 .collect()
1330}
1331
1332fn clip_to_circle(lines: Vec<Vec<[f64; 2]>>, centre: [f64; 2], radius: f64) -> Vec<Vec<[f64; 2]>> {
1341 let mut out: Vec<Vec<[f64; 2]>> = Vec::new();
1342 let flush = |current: &mut Vec<[f64; 2]>, out: &mut Vec<Vec<[f64; 2]>>| {
1343 if current.len() >= 2 {
1344 out.push(std::mem::take(current));
1345 } else {
1346 current.clear();
1347 }
1348 };
1349 for line in lines {
1350 let mut current: Vec<[f64; 2]> = Vec::new();
1351 for pair in line.windows(2) {
1352 let (p, q) = (pair[0], pair[1]);
1353 let d = [q[0] - p[0], q[1] - p[1]];
1354 let f = [p[0] - centre[0], p[1] - centre[1]];
1355 let a = d[0] * d[0] + d[1] * d[1];
1356 let c = f[0] * f[0] + f[1] * f[1] - radius * radius;
1357 let span = if a < 1e-24 {
1359 (c <= 0.0).then_some((0.0, 1.0))
1360 } else {
1361 let b = 2.0 * (f[0] * d[0] + f[1] * d[1]);
1362 let disc = b * b - 4.0 * a * c;
1363 if disc < 0.0 {
1364 None
1365 } else {
1366 let root = disc.sqrt();
1367 let (t0, t1) = (((-b - root) / (2.0 * a)).max(0.0), ((-b + root) / (2.0 * a)).min(1.0));
1368 (t1 > t0).then_some((t0, t1))
1369 }
1370 };
1371 let at = |t: f64| [p[0] + d[0] * t, p[1] + d[1] * t];
1372 match span {
1373 None => flush(&mut current, &mut out),
1374 Some((t0, t1)) => {
1375 if t0 <= 0.0 {
1376 if current.is_empty() {
1377 current.push(p);
1378 }
1379 if t1 >= 1.0 {
1380 current.push(q);
1381 } else {
1382 current.push(at(t1));
1383 flush(&mut current, &mut out);
1384 }
1385 } else {
1386 flush(&mut current, &mut out);
1387 current.push(at(t0));
1388 if t1 >= 1.0 {
1389 current.push(q);
1390 } else {
1391 current.push(at(t1));
1392 flush(&mut current, &mut out);
1393 }
1394 }
1395 }
1396 }
1397 }
1398 flush(&mut current, &mut out);
1399 }
1400 out
1401}
1402
1403fn bounds_of(view: &ViewDrawing, placed: &PlacedView) -> [f64; 4] {
1404 let mut bounds = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
1405 let mut add = |p: [f64; 2]| {
1406 bounds[0] = bounds[0].min(p[0]);
1407 bounds[1] = bounds[1].min(p[1]);
1408 bounds[2] = bounds[2].max(p[0]);
1409 bounds[3] = bounds[3].max(p[1]);
1410 };
1411 for polyline in &view.edges {
1412 for point in polyline {
1413 add(*point);
1414 }
1415 }
1416 for annotation in &view.annotations {
1417 for polyline in &annotation.lines {
1418 for point in polyline {
1419 add(*point);
1420 }
1421 }
1422 for text in &annotation.texts {
1423 add(text.anchor);
1424 }
1425 }
1426 if let Some(section) = &view.section {
1427 for polyline in section.hatch.iter().chain(section.cut.iter()) {
1428 for point in polyline {
1429 add(*point);
1430 }
1431 }
1432 for text in §ion.texts {
1433 add(text.anchor);
1434 }
1435 }
1436 for mark in &view.section_marks {
1437 for polyline in &mark.lines {
1438 for point in polyline {
1439 add(*point);
1440 }
1441 }
1442 for text in &mark.texts {
1443 add(text.anchor);
1444 }
1445 }
1446 if let Some(detail) = &view.detail {
1447 for point in &detail.circle {
1448 add(*point);
1449 }
1450 for text in &detail.texts {
1451 add(text.anchor);
1452 }
1453 }
1454 for mark in &view.detail_marks {
1455 for polyline in &mark.lines {
1456 for point in polyline {
1457 add(*point);
1458 }
1459 }
1460 for text in &mark.texts {
1461 add(text.anchor);
1462 }
1463 }
1464 if bounds[0] > bounds[2] {
1465 [placed.position[0], placed.position[1], placed.position[0], placed.position[1]]
1466 } else {
1467 bounds
1468 }
1469}
1470
1471#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
1478pub struct SolidPose {
1479 pub translate: [f64; 3],
1480 #[serde(rename = "rotateDeg")]
1481 pub rotate_deg: [f64; 3],
1482 pub scale: [f64; 3],
1483 pub center: [f64; 3],
1484}
1485
1486impl SolidPose {
1487 pub fn apply(&self, p: [f64; 3]) -> [f64; 3] {
1488 let (center, scale, translate) = (self.center, self.scale, self.translate);
1489 let local = [(p[0] - center[0]) * scale[0], (p[1] - center[1]) * scale[1], (p[2] - center[2]) * scale[2]];
1490 let rotated = crate::engine_state::rotate_euler_xyz_f64(local, self.rotate_deg.map(f64::to_radians));
1492 [
1493 rotated[0] + center[0] + translate[0],
1494 rotated[1] + center[1] + translate[1],
1495 rotated[2] + center[2] + translate[2],
1496 ]
1497 }
1498
1499 pub fn matrix(&self) -> [f64; 16] {
1502 let radians = self.rotate_deg.map(f64::to_radians);
1503 let column = |axis: [f64; 3]| crate::engine_state::rotate_euler_xyz_f64(axis, radians);
1504 let cols = [
1505 column([self.scale[0], 0.0, 0.0]),
1506 column([0.0, self.scale[1], 0.0]),
1507 column([0.0, 0.0, self.scale[2]]),
1508 ];
1509 let c = self.center;
1510 let lc = [
1511 cols[0][0] * c[0] + cols[1][0] * c[1] + cols[2][0] * c[2],
1512 cols[0][1] * c[0] + cols[1][1] * c[1] + cols[2][1] * c[2],
1513 cols[0][2] * c[0] + cols[1][2] * c[1] + cols[2][2] * c[2],
1514 ];
1515 let t = [c[0] + self.translate[0] - lc[0], c[1] + self.translate[1] - lc[1], c[2] + self.translate[2] - lc[2]];
1516 [
1517 cols[0][0], cols[0][1], cols[0][2], 0.0,
1518 cols[1][0], cols[1][1], cols[1][2], 0.0,
1519 cols[2][0], cols[2][1], cols[2][2], 0.0,
1520 t[0], t[1], t[2], 1.0,
1521 ]
1522 }
1523
1524 fn identity(&self) -> String {
1526 let bits = |v: &[f64; 3]| format!("{:x},{:x},{:x}", v[0].to_bits(), v[1].to_bits(), v[2].to_bits());
1527 format!(
1528 "pose[{}|{}|{}|{}];",
1529 bits(&self.translate),
1530 bits(&self.rotate_deg),
1531 bits(&self.scale),
1532 bits(&self.center)
1533 )
1534 }
1535}
1536
1537pub(crate) fn poses_of(report: Option<&PmiViewReport>, name: &str) -> Vec<SolidPose> {
1541 let Some(report) = report else { return Vec::new() };
1542 report
1543 .annotations
1544 .iter()
1545 .filter(|row| row.enabled && row.status == PmiStatus::Ok)
1546 .filter_map(|row| match &row.geometry {
1547 brep_kernel::PmiGeometry::Explode { solids, translate, rotate_deg, scale, center, .. }
1548 if solids.iter().any(|target| target == name) =>
1549 {
1550 Some(SolidPose { translate: *translate, rotate_deg: *rotate_deg, scale: *scale, center: *center })
1551 }
1552 _ => None,
1553 })
1554 .collect()
1555}
1556
1557#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1561pub struct LinesSolid {
1562 pub name: String,
1563 pub handle: u32,
1564 pub fingerprint: u64,
1565 pub poses: Vec<SolidPose>,
1566 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1569 pub whole: bool,
1570}
1571
1572impl LinesSolid {
1573 fn identity(&self) -> String {
1578 let mut identity = format!("{}#{:016x};", self.name, self.fingerprint);
1579 for pose in &self.poses {
1580 identity.push_str(&pose.identity());
1581 }
1582 if self.whole {
1583 identity.push_str("whole;");
1584 }
1585 identity
1586 }
1587}
1588
1589struct ExactPlan<'a> {
1593 solids: Vec<LinesSolid>,
1594 sources: Vec<&'a brep_kernel::BrepSolid>,
1596 each: Vec<String>,
1598 identity: String,
1599}
1600
1601impl ExactPlan<'_> {
1602 fn posed(&self) -> Vec<std::borrow::Cow<'_, brep_kernel::BrepSolid>> {
1609 self.solids
1610 .iter()
1611 .zip(&self.sources)
1612 .map(|(solid, source)| posed_solid(std::borrow::Cow::Borrowed(*source), &solid.poses))
1613 .collect()
1614 }
1615}
1616
1617fn posed_solid<'a>(
1618 solid: std::borrow::Cow<'a, brep_kernel::BrepSolid>,
1619 poses: &[SolidPose],
1620) -> std::borrow::Cow<'a, brep_kernel::BrepSolid> {
1621 poses.iter().fold(solid, |solid, pose| std::borrow::Cow::Owned(pose_solid(&solid, &|p| pose.apply(p))))
1622}
1623
1624fn left_whole(view: &PmiView, name: &str) -> bool {
1626 view.display.section.as_ref().is_some_and(|section| section.excludes(name))
1627}
1628
1629fn exact_solids<'a>(scene: &'a RenderScene, unposed: &Unposed, view: &PmiView, report: Option<&PmiViewReport>) -> Option<ExactPlan<'a>> {
1632 let mut plan = ExactPlan { solids: Vec::new(), sources: Vec::new(), each: Vec::new(), identity: String::new() };
1633 for solid in drawn_solids(scene, unposed, view) {
1634 let (brep, fingerprint) = scene.exact_solid_fingerprinted(&solid.name)?;
1635 let planned = LinesSolid {
1636 name: solid.name.clone(),
1637 handle: solid.source_handle,
1638 fingerprint,
1639 poses: poses_of(report, &solid.name),
1640 whole: left_whole(view, &solid.name),
1641 };
1642 plan.each.push(planned.identity());
1643 plan.solids.push(planned);
1644 plan.sources.push(brep);
1645 }
1646 plan.identity = plan.each.concat();
1647 Some(plan)
1648}
1649
1650#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
1658pub enum LinesClip {
1659 Whole,
1660 Circle { radius: f64 },
1661 Section { half: Option<super::hlr::HalfLine> },
1662}
1663
1664impl LinesClip {
1665 fn key(&self) -> String {
1667 match self {
1668 LinesClip::Whole => "whole".into(),
1669 LinesClip::Circle { radius } => format!("circle {:x}", radius.to_bits()),
1670 LinesClip::Section { half: None } => "section".into(),
1671 LinesClip::Section { half: Some(half) } => format!(
1672 "section half {:x},{:x},{:x}",
1673 half.normal[0].to_bits(),
1674 half.normal[1].to_bits(),
1675 half.offset.to_bits()
1676 ),
1677 }
1678 }
1679}
1680
1681#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1685pub struct LinesJob {
1686 pub key: String,
1688 pub placement: String,
1691 pub solids: Vec<LinesSolid>,
1692 pub frame: ViewFrame,
1693 pub placed: PlacedView,
1695 pub clip: LinesClip,
1696}
1697
1698pub enum LinesMode<'a> {
1700 Compute,
1703 Defer(&'a mut dyn FnMut(LinesJob) -> Deferred),
1708 Export(&'a mut dyn FnMut(LinesJob) -> Deferred),
1710}
1711
1712pub enum Deferred {
1714 Lines(ExactLines),
1715 Pending,
1716 Here,
1717}
1718
1719pub fn run_lines_job(job: &LinesJob) -> Result<ExactLines, String> {
1725 let mut posed = Vec::with_capacity(job.solids.len());
1726 for solid in &job.solids {
1727 let brep = brep_kernel::registered_solid_clone(solid.handle)
1728 .map_err(|error| format!("'{}' (handle {}): {error}", solid.name, solid.handle))?;
1729 if crate::scene::brep_fingerprint(&brep) != solid.fingerprint {
1730 return Err(format!("'{}' changed under handle {}", solid.name, solid.handle));
1731 }
1732 posed.push(posed_solid(std::borrow::Cow::Owned(brep), &solid.poses).into_owned());
1733 }
1734 let solids: Vec<&brep_kernel::BrepSolid> = posed.iter().collect();
1735 let each: Vec<String> = job.solids.iter().map(LinesSolid::identity).collect();
1736 let whole: Vec<bool> = job.solids.iter().map(|solid| solid.whole).collect();
1737 Ok(compute_exact_lines(&solids, &each, &whole, &job.frame, &job.placed, job.clip))
1738}
1739
1740fn compute_exact_lines(
1743 solids: &[&brep_kernel::BrepSolid],
1744 each: &[String],
1745 whole: &[bool],
1746 frame: &ViewFrame,
1747 origin: &PlacedView,
1748 clip: LinesClip,
1749) -> ExactLines {
1750 let trims = solid_trims(each, solids);
1751 let trims: Vec<&super::hlr::SolidTrims> = trims.iter().map(|t| t.as_ref()).collect();
1752 match clip {
1753 LinesClip::Whole => ExactLines {
1754 runs: super::hlr::visible_edges_with(solids, &trims, frame, origin, &super::hlr::Clip::Whole).runs,
1755 cut: Vec::new(),
1756 },
1757 LinesClip::Circle { radius } => ExactLines {
1758 runs: super::hlr::visible_edges_with(solids, &trims, frame, origin, &super::hlr::Clip::Circle { radius }).runs,
1759 cut: Vec::new(),
1760 },
1761 LinesClip::Section { half } => {
1762 let cut_by = |index: &usize| !whole.get(*index).copied().unwrap_or(false);
1768 let cut_solids: Vec<&brep_kernel::BrepSolid> = (0..solids.len()).filter(cut_by).map(|i| solids[i]).collect();
1769 let cut_trims: Vec<&super::hlr::SolidTrims> = (0..solids.len()).filter(cut_by).map(|i| trims[i]).collect();
1770 let cut = super::hlr::section_curves_with(&cut_solids, &cut_trims, frame, origin);
1771 let cut = match half {
1772 Some(half) => close_on_half(cut, half, origin),
1773 None => cut,
1774 };
1775 let clip = super::hlr::Clip::Beyond { cap: &cut, whole, half };
1776 ExactLines { runs: super::hlr::visible_edges_with(solids, &trims, frame, origin, &clip).runs, cut }
1777 }
1778 }
1779}
1780
1781fn exact_lines_for(
1785 plan: &ExactPlan,
1786 frame: &ViewFrame,
1787 placed: &PlacedView,
1788 clip: LinesClip,
1789 mode: &mut LinesMode,
1790) -> Option<ExactLines> {
1791 let key = exact_key(&plan.identity, frame, placed.scale, &clip.key());
1792 let origin = PlacedView { position: [0.0, 0.0], ..placed.clone() };
1793 let relative = match cached_exact_lines(&key) {
1794 Some(lines) => lines,
1795 None => {
1796 let deferred = match mode {
1797 LinesMode::Compute => Deferred::Here,
1798 LinesMode::Defer(defer) | LinesMode::Export(defer) => defer(LinesJob {
1799 key: key.clone(),
1800 placement: placed.id.clone(),
1801 solids: plan.solids.clone(),
1802 frame: *frame,
1803 placed: origin.clone(),
1804 clip,
1805 }),
1806 };
1807 let lines = match deferred {
1808 Deferred::Lines(lines) => lines,
1809 Deferred::Pending => return None,
1810 Deferred::Here => {
1811 let posed = plan.posed();
1812 let solids: Vec<&brep_kernel::BrepSolid> = posed.iter().map(|solid| solid.as_ref()).collect();
1813 let whole: Vec<bool> = plan.solids.iter().map(|solid| solid.whole).collect();
1814 compute_exact_lines(&solids, &plan.each, &whole, frame, &origin, clip)
1815 }
1816 };
1817 store_exact_lines(key, lines.clone());
1818 lines
1819 }
1820 };
1821 let shift = |lines: Vec<Vec<[f64; 2]>>| -> Vec<Vec<[f64; 2]>> {
1822 lines
1823 .into_iter()
1824 .map(|line| line.into_iter().map(|p| [p[0] + placed.position[0], p[1] + placed.position[1]]).collect())
1825 .collect()
1826 };
1827 Some(ExactLines { runs: shift(relative.runs), cut: shift(relative.cut) })
1828}
1829
1830const TRIM_CACHE_SOLIDS: usize = EXACT_CACHE_ENTRIES;
1833
1834thread_local! {
1835 static TRIM_CACHE: std::cell::RefCell<std::collections::VecDeque<(String, std::rc::Rc<super::hlr::SolidTrims>)>> =
1840 const { std::cell::RefCell::new(std::collections::VecDeque::new()) };
1841}
1842
1843
1844
1845fn solid_trims(identities: &[String], solids: &[&brep_kernel::BrepSolid]) -> Vec<std::rc::Rc<super::hlr::SolidTrims>> {
1847 TRIM_CACHE.with(|cache| {
1848 let mut cache = cache.borrow_mut();
1849 identities
1850 .iter()
1851 .zip(solids)
1852 .map(|(identity, solid)| {
1853 let trims = match cache.iter().position(|(key, _)| key == identity) {
1854 Some(index) => cache.remove(index).map(|(_, trims)| trims).unwrap(),
1855 None => std::rc::Rc::new(super::hlr::SolidTrims::of(solid)),
1856 };
1857 cache.push_back((identity.clone(), trims.clone()));
1858 while cache.len() > TRIM_CACHE_SOLIDS {
1859 cache.pop_front();
1860 }
1861 trims
1862 })
1863 .collect()
1864 })
1865}
1866
1867#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
1871pub struct ExactLines {
1872 pub runs: Vec<Vec<[f64; 2]>>,
1873 pub cut: Vec<Vec<[f64; 2]>>,
1874}
1875
1876const EXACT_CACHE_ENTRIES: usize = 64;
1878
1879thread_local! {
1880 static EXACT_CACHE: std::cell::RefCell<std::collections::VecDeque<(String, ExactLines)>> =
1887 const { std::cell::RefCell::new(std::collections::VecDeque::new()) };
1888}
1889
1890pub(crate) fn cached_exact_lines(key: &str) -> Option<ExactLines> {
1892 EXACT_CACHE.with(|cache| cache.borrow().iter().find(|(k, _)| k == key).map(|(_, lines)| lines.clone()))
1893}
1894
1895pub(crate) fn store_exact_lines(key: String, lines: ExactLines) {
1898 EXACT_CACHE.with(|cache| {
1899 let mut cache = cache.borrow_mut();
1900 cache.retain(|(k, _)| *k != key);
1901 cache.push_back((key, lines));
1902 while cache.len() > EXACT_CACHE_ENTRIES {
1903 cache.pop_front();
1904 }
1905 });
1906}
1907
1908fn exact_key(identity: &str, frame: &ViewFrame, scale: f64, clip: &str) -> String {
1910 let bits = |v: [f64; 3]| format!("{:x},{:x},{:x}", v[0].to_bits(), v[1].to_bits(), v[2].to_bits());
1911 format!(
1912 "{identity}|{}|{}|{}|{}|{:x}|{clip}",
1913 bits(frame.target),
1914 bits(frame.right),
1915 bits(frame.up),
1916 bits(frame.forward),
1917 scale.to_bits()
1918 )
1919}
1920
1921fn pose_solid(solid: &brep_kernel::BrepSolid, pose: &dyn Fn([f64; 3]) -> [f64; 3]) -> brep_kernel::BrepSolid {
1928 let map = |cp: &brep_kernel::Vec4| -> brep_kernel::Vec4 {
1929 let w = if cp.w != 0.0 { cp.w } else { 1.0 };
1930 let p = pose([cp.x / w, cp.y / w, cp.z / w]);
1931 brep_kernel::Vec4 { x: p[0] * w, y: p[1] * w, z: p[2] * w, w: cp.w }
1932 };
1933 let mut out = solid.clone();
1934 for vertex in &mut out.vertices {
1935 let p = pose([vertex.point.x, vertex.point.y, vertex.point.z]);
1936 vertex.point = brep_kernel::Vec3::new(p[0], p[1], p[2]);
1937 }
1938 for edge in &mut out.edges {
1939 let points = edge.curve.control_points.iter().map(map).collect();
1940 if let Ok(curve) = brep_kernel::NurbsCurve::new(edge.curve.degree, edge.curve.knots.clone(), points) {
1941 edge.curve = curve;
1942 }
1943 }
1944 for shell in &mut out.shells {
1945 for face in &mut shell.faces {
1946 let s = &face.surface;
1947 let points = s.control_points.iter().map(|row| row.iter().map(map).collect()).collect();
1948 if let Ok(surface) =
1949 brep_kernel::NurbsSurface::new(s.degree_u, s.degree_v, s.knots_u.clone(), s.knots_v.clone(), points)
1950 {
1951 face.surface = surface;
1952 }
1953 }
1954 }
1955 out
1956}
1957
1958fn close_on_half(cut: Vec<Vec<[f64; 2]>>, half: super::hlr::HalfLine, origin: &PlacedView) -> Vec<Vec<[f64; 2]>> {
1965 let side = |p: [f64; 2]| {
1966 half.at([(p[0] - origin.position[0]) / origin.scale, (origin.position[1] - p[1]) / origin.scale, 0.0])
1967 };
1968 let mut out: Vec<Vec<[f64; 2]>> = Vec::new();
1969 let mut crossings: Vec<[f64; 2]> = Vec::new();
1970 for line in cut {
1971 let mut run: Vec<[f64; 2]> = Vec::new();
1972 for (i, &p) in line.iter().enumerate() {
1973 let g = side(p);
1974 if i > 0 {
1975 let q = line[i - 1];
1976 let h = side(q);
1977 if (h < 0.0) != (g < 0.0) {
1978 let t = h / (h - g);
1979 let x = [q[0] + (p[0] - q[0]) * t, q[1] + (p[1] - q[1]) * t];
1980 crossings.push(x);
1981 run.push(x);
1982 if g >= 0.0 {
1983 out.push(std::mem::take(&mut run));
1984 }
1985 }
1986 }
1987 if g < 0.0 {
1988 run.push(p);
1989 }
1990 }
1991 if run.len() > 1 {
1992 out.push(run);
1993 }
1994 }
1995 let along = |p: &[f64; 2]| -p[0] * half.normal[1] - p[1] * half.normal[0];
1998 crossings.sort_by(|a, b| along(a).partial_cmp(&along(b)).unwrap_or(std::cmp::Ordering::Equal));
1999 out.extend(crossings.chunks_exact(2).map(|pair| vec![pair[0], pair[1]]));
2000 out.retain(|line| line.len() > 1);
2001 out
2002}
2003
2004fn segments_of(lines: &[Vec<[f64; 2]>]) -> Vec<Vec<[f64; 2]>> {
2005 lines.iter().flat_map(|line| line.windows(2).map(|w| vec![w[0], w[1]])).collect()
2006}
2007
2008fn drawn_solids<'a>(scene: &'a RenderScene, unposed: &'a Unposed, view: &PmiView) -> Vec<&'a crate::scene::SolidDisplay> {
2018 scene
2019 .solids()
2020 .iter()
2021 .map(|solid| unposed.get(&solid.name).unwrap_or(solid))
2022 .filter(|solid| solid.visible && !solid.is_sketch)
2023 .filter(|solid| !view.hides(&solid.name))
2024 .filter(|solid| solid.mesh.indices.len() >= 3)
2025 .collect()
2026}
2027
2028pub fn sheet_solid_names(scene: &RenderScene, unposed: &Unposed, sheet: &super::Sheet, views: &[PmiView]) -> Vec<String> {
2031 let mut names: Vec<String> = Vec::new();
2032 for placed in &sheet.views {
2033 let Some(view) = views.iter().find(|view| view.id == placed.view) else { continue };
2034 for solid in drawn_solids(scene, unposed, view) {
2035 if !names.contains(&solid.name) {
2036 names.push(solid.name.clone());
2037 }
2038 }
2039 }
2040 names
2041}
2042
2043fn drawn_displays<'a>(scene: &'a RenderScene, unposed: &'a Unposed, view: &PmiView) -> Vec<&'a crate::scene::SolidDisplay> {
2047 drawn_solids(scene, unposed, view)
2048}
2049
2050
2051fn mesh_solids(displays: &[&crate::scene::SolidDisplay], report: Option<&PmiViewReport>) -> Vec<super::hlr::MeshSolid> {
2067 let world = |p: &[f32; 3]| [p[0] as f64, p[1] as f64, p[2] as f64];
2068 displays
2069 .iter()
2070 .map(|solid| {
2071 let poses = poses_of(report, &solid.name);
2072 let pose = |p: [f64; 3]| poses.iter().fold(p, |p, pose| pose.apply(p));
2073 let count = solid.mesh.positions.len();
2074 let (triangles, faces) = solid
2075 .mesh
2076 .indices
2077 .chunks_exact(3)
2078 .enumerate()
2079 .filter(|(_, c)| c.iter().all(|i| (*i as usize) < count))
2080 .map(|(t, c)| ([c[0], c[1], c[2]], solid.mesh.face_ids.get(t).copied().unwrap_or(u32::MAX)))
2081 .unzip();
2082 let bits = |p: [f32; 3]| [p[0].to_bits(), p[1].to_bits(), p[2].to_bits()];
2083 let mut at: std::collections::HashMap<[u32; 3], Option<u64>> = std::collections::HashMap::new();
2084 for vertex in &solid.vertices {
2085 let p = vertex.position;
2086 at.entry(bits([p[0] as f32, p[1] as f32, p[2] as f32]))
2087 .and_modify(|id| *id = None)
2088 .or_insert(Some(vertex.topo_id));
2089 }
2090 let vertex_of = |p: &[f32; 3]| at.get(&bits(*p)).copied().flatten();
2091 super::hlr::MeshSolid {
2092 positions: solid.mesh.positions.iter().map(|p| pose(world(p))).collect(),
2093 triangles,
2094 faces,
2095 edges: solid
2096 .edges
2097 .iter()
2098 .filter(|edge| !edge.aux && !edge.centerline && edge.polyline.len() >= 2)
2099 .map(|edge| super::hlr::MeshEdge {
2100 points: edge.polyline.iter().map(|p| pose(world(p))).collect(),
2101 ends: vertex_of(&edge.polyline[0]).zip(vertex_of(edge.polyline.last().unwrap())),
2102 })
2103 .collect(),
2104 }
2105 })
2106 .collect()
2107}
2108
2109const MESH_CACHE_ENTRIES: usize = 16;
2111
2112thread_local! {
2113 static MESH_CACHE: std::cell::RefCell<std::collections::VecDeque<(String, ExactLines)>> =
2120 const { std::cell::RefCell::new(std::collections::VecDeque::new()) };
2121}
2122
2123#[allow(clippy::too_many_arguments)]
2126fn mesh_lines_for(
2127 scene: &RenderScene,
2128 unposed: &Unposed,
2129 view: &PmiView,
2130 report: Option<&PmiViewReport>,
2131 frame: &ViewFrame,
2132 placed: &PlacedView,
2133 clip: LinesClip,
2134 plane: Option<&ClipPlane>,
2135) -> ExactLines {
2136 let displays = drawn_displays(scene, unposed, view);
2137 let mut identity = String::from("mesh|");
2138 let whole: Vec<bool> = displays.iter().map(|display| left_whole(view, &display.name)).collect();
2139 for (display, whole) in displays.iter().zip(&whole) {
2140 identity.push_str(&format!("{}@{};", display.name, display.revision));
2141 for pose in poses_of(report, &display.name) {
2142 identity.push_str(&pose.identity());
2143 }
2144 if *whole {
2145 identity.push_str("whole;");
2146 }
2147 }
2148 let bits = |v: [f64; 3]| format!("{:x},{:x},{:x}", v[0].to_bits(), v[1].to_bits(), v[2].to_bits());
2149 let plane_key = plane.map(|plane| format!("{}|{}", bits(plane.point), bits(plane.normal))).unwrap_or_default();
2150 let key = exact_key(&identity, frame, placed.scale, &format!("{}{plane_key}", clip.key()));
2151 let hit = MESH_CACHE.with(|cache| cache.borrow().iter().find(|(k, _)| *k == key).map(|(_, lines)| lines.clone()));
2152 let relative = match hit {
2153 Some(lines) => lines,
2154 None => {
2155 let origin = PlacedView { position: [0.0, 0.0], ..placed.clone() };
2156 let meshes = mesh_solids(&displays, report);
2157 let lines = match (clip, plane) {
2158 (LinesClip::Section { half }, Some(plane)) => {
2159 let cut_meshes: Vec<_> = meshes.iter().zip(&whole).filter(|(_, whole)| !**whole).map(|(mesh, _)| mesh.clone()).collect();
2160 let cut: Vec<Vec<[f64; 2]>> = mesh_cut(&cut_meshes, plane)
2161 .iter()
2162 .map(|segment| {
2163 segment
2164 .iter()
2165 .map(|p| {
2166 let v = frame.to_view(*p);
2167 to_paper(&origin, v[0], v[1])
2168 })
2169 .collect()
2170 })
2171 .collect();
2172 let cut = match half {
2173 Some(half) => close_on_half(cut, half, &origin),
2174 None => cut,
2175 };
2176 let beyond = super::hlr::Clip::Beyond { cap: &cut, whole: &whole, half };
2177 ExactLines { runs: super::hlr::visible_mesh_lines(&meshes, frame, &origin, &beyond).runs, cut }
2178 }
2179 (LinesClip::Circle { radius }, _) => ExactLines {
2180 runs: super::hlr::visible_mesh_lines(&meshes, frame, &origin, &super::hlr::Clip::Circle { radius }).runs,
2181 cut: Vec::new(),
2182 },
2183 _ => ExactLines {
2184 runs: super::hlr::visible_mesh_lines(&meshes, frame, &origin, &super::hlr::Clip::Whole).runs,
2185 cut: Vec::new(),
2186 },
2187 };
2188 MESH_CACHE.with(|cache| {
2189 let mut cache = cache.borrow_mut();
2190 cache.push_back((key, lines.clone()));
2191 while cache.len() > MESH_CACHE_ENTRIES {
2192 cache.pop_front();
2193 }
2194 });
2195 lines
2196 }
2197 };
2198 let shift = |lines: Vec<Vec<[f64; 2]>>| -> Vec<Vec<[f64; 2]>> {
2199 lines
2200 .into_iter()
2201 .map(|line| line.into_iter().map(|p| [p[0] + placed.position[0], p[1] + placed.position[1]]).collect())
2202 .collect()
2203 };
2204 ExactLines { runs: shift(relative.runs), cut: shift(relative.cut) }
2205}
2206
2207#[derive(Debug, Clone, Copy)]
2222pub struct ClipPlane {
2223 pub point: [f64; 3],
2224 pub normal: [f64; 3],
2225}
2226
2227fn mesh_cut(meshes: &[super::hlr::MeshSolid], plane: &ClipPlane) -> Vec<[[f64; 3]; 2]> {
2232 let mut extent: f64 = 0.0;
2236 for mesh in meshes {
2237 for p in &mesh.positions {
2238 extent = extent.max(dot(sub(*p, plane.point), plane.normal).abs());
2239 }
2240 }
2241 let eps = (extent * 1e-9).max(f64::MIN_POSITIVE);
2242
2243 let mut cut: Vec<[[f64; 3]; 2]> = Vec::new();
2244 for mesh in meshes {
2245 let side: Vec<f64> = mesh
2246 .positions
2247 .iter()
2248 .map(|p| dot(sub(*p, plane.point), plane.normal))
2249 .collect();
2250 for tri in &mesh.triangles {
2251 let corners = [tri[0] as usize, tri[1] as usize, tri[2] as usize];
2252 let d = [side[corners[0]], side[corners[1]], side[corners[2]]];
2253 if d.iter().all(|v| *v < -eps) || d.iter().all(|v| *v > eps) {
2254 continue; }
2256 let mut kept = 0;
2259 let mut on_plane: Vec<[f64; 3]> = Vec::with_capacity(2);
2260 for k in 0..3 {
2261 let (i, j) = (k, (k + 1) % 3);
2262 let (pi, pj) = (mesh.positions[corners[i]], mesh.positions[corners[j]]);
2263 let (di, dj) = (d[i], d[j]);
2264 if di >= -eps {
2265 kept += 1;
2266 if di.abs() <= eps {
2267 on_plane.push(pi);
2268 }
2269 }
2270 if (di > eps && dj < -eps) || (di < -eps && dj > eps) {
2271 let t = di / (di - dj);
2272 kept += 1;
2273 on_plane.push([
2274 pi[0] + (pj[0] - pi[0]) * t,
2275 pi[1] + (pj[1] - pi[1]) * t,
2276 pi[2] + (pj[2] - pi[2]) * t,
2277 ]);
2278 }
2279 }
2280 if kept >= 3 && on_plane.len() == 2 {
2294 cut.push([on_plane[0], on_plane[1]]);
2295 }
2296 }
2297 }
2298 cut
2299}
2300
2301fn hatch(cut: &[Vec<[f64; 2]>]) -> Vec<Vec<[f64; 2]>> {
2311 if cut.is_empty() {
2312 return Vec::new();
2313 }
2314 let d = std::f64::consts::FRAC_1_SQRT_2;
2317 let u = [d, -d];
2318 let n = [d, d];
2319 let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
2320 for segment in cut {
2321 for p in segment {
2322 let c = p[0] * n[0] + p[1] * n[1];
2323 lo = lo.min(c);
2324 hi = hi.max(c);
2325 }
2326 }
2327 if !lo.is_finite() || hi <= lo {
2328 return Vec::new();
2329 }
2330 let mut spacing = HATCH_SPACING_MM;
2331 while (hi - lo) / spacing > MAX_HATCH_LINES as f64 {
2332 spacing *= 2.0;
2333 }
2334 let first = (lo / spacing).ceil() * spacing;
2337 let mut out = Vec::new();
2338 let mut c = first;
2339 while c <= hi {
2340 let mut crossings: Vec<f64> = Vec::new();
2341 for segment in cut {
2342 let (a, b) = (segment[0], segment[segment.len() - 1]);
2343 let (ca, cb) = (a[0] * n[0] + a[1] * n[1], b[0] * n[0] + b[1] * n[1]);
2344 if (ca <= c) == (cb <= c) {
2347 continue;
2348 }
2349 let t = (c - ca) / (cb - ca);
2350 let hit = [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
2351 crossings.push(hit[0] * u[0] + hit[1] * u[1]);
2352 }
2353 crossings.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
2354 for pair in crossings.chunks_exact(2) {
2355 if pair[1] - pair[0] < 1e-9 {
2356 continue;
2357 }
2358 out.push(vec![
2359 [u[0] * pair[0] + n[0] * c, u[1] * pair[0] + n[1] * c],
2360 [u[0] * pair[1] + n[0] * c, u[1] * pair[1] + n[1] * c],
2361 ]);
2362 }
2363 c += spacing;
2364 }
2365 out
2366}
2367
2368const CIRCLE_MIN_SPAN_DEG: f64 = 20.0;
2377
2378const CIRCLE_FIT_TOLERANCE: f64 = 1e-4;
2383
2384const STRAIGHT_TOLERANCE: f64 = 1e-4;
2389
2390fn anchor_points(
2398 scene: &RenderScene,
2399 unposed: &Unposed,
2400 view: &PmiView,
2401 report: Option<&PmiViewReport>,
2402 frame: &ViewFrame,
2403 placed: &PlacedView,
2404) -> Vec<AnchorPoint> {
2405 let project = |p: [f64; 3]| -> [f64; 2] {
2406 let v = frame.to_view(p);
2407 to_paper(placed, v[0], v[1])
2408 };
2409 let mut out = Vec::new();
2410 for solid in drawn_displays(scene, unposed, view) {
2411 let poses = poses_of(report, &solid.name);
2414 let pose = |p: [f64; 3]| poses.iter().fold(p, |p, pose| pose.apply(p));
2415 for vertex in &solid.vertices {
2416 out.push(AnchorPoint {
2417 anchor: super::dimension::SheetAnchor::vertex(&placed.id, &solid.name, vertex.topo_id)
2418 .to_ref(),
2419 kind: super::dimension::VERTEX.to_string(),
2420 at: project(pose(vertex.position)),
2421 model: pose(vertex.position),
2422 dir: None,
2423 radius: None,
2424 u: None,
2425 v: None,
2426 });
2427 }
2428 for edge in &solid.edges {
2429 if edge.name.is_empty() || edge.aux || edge.centerline || edge.polyline.len() < 2 {
2433 continue;
2434 }
2435 let points: Vec<[f64; 3]> = edge
2436 .polyline
2437 .iter()
2438 .map(|p| pose([p[0] as f64, p[1] as f64, p[2] as f64]))
2439 .collect();
2440 let dir = straight_chord(&points).and_then(|d| {
2445 let paper = [dot(d, frame.right), -dot(d, frame.up)];
2446 let length = (paper[0] * paper[0] + paper[1] * paper[1]).sqrt();
2447 (length > 1e-9).then(|| [paper[0] / length, paper[1] / length])
2448 });
2449 for fraction in [0.0, 0.5, 1.0] {
2450 let Some(point) = along(&points, fraction) else { continue };
2451 out.push(AnchorPoint {
2452 anchor: super::dimension::SheetAnchor::edge(
2453 &placed.id,
2454 &solid.name,
2455 &edge.name,
2456 fraction,
2457 )
2458 .to_ref(),
2459 kind: super::dimension::EDGE.to_string(),
2460 at: project(point),
2461 model: point,
2462 dir,
2463 radius: None,
2464 u: None,
2465 v: None,
2466 });
2467 }
2468 if let Some((centre, axis, radius)) = fit_circle(&points) {
2469 let u3 = norm(sub(points[0], centre));
2473 let v3 = cross(axis, u3);
2474 let image = |d: [f64; 3]| -> [f64; 2] {
2475 [
2476 dot(d, frame.right) * radius * placed.scale,
2477 -dot(d, frame.up) * radius * placed.scale,
2478 ]
2479 };
2480 out.push(AnchorPoint {
2481 anchor: super::dimension::SheetAnchor::circle(&placed.id, &solid.name, &edge.name)
2482 .to_ref(),
2483 kind: super::dimension::CIRCLE.to_string(),
2484 at: project(centre),
2485 model: centre,
2486 dir: None,
2487 radius: Some(radius),
2488 u: Some(image(u3)),
2489 v: Some(image(v3)),
2490 });
2491 }
2492 }
2493 }
2494 out
2495}
2496
2497fn straight_chord(points: &[[f64; 3]]) -> Option<[f64; 3]> {
2505 let (first, last) = (*points.first()?, *points.last()?);
2506 let span = sub(last, first);
2507 let length = dot(span, span).sqrt();
2508 if length < 1e-12 {
2509 return None;
2511 }
2512 let direction = [span[0] / length, span[1] / length, span[2] / length];
2513 let bar = length * STRAIGHT_TOLERANCE;
2514 for point in points {
2515 let offset = sub(*point, first);
2516 let along = dot(offset, direction);
2517 let away = [
2518 offset[0] - direction[0] * along,
2519 offset[1] - direction[1] * along,
2520 offset[2] - direction[2] * along,
2521 ];
2522 if dot(away, away).sqrt() > bar {
2523 return None;
2524 }
2525 }
2526 Some(direction)
2527}
2528
2529fn along(points: &[[f64; 3]], fraction: f64) -> Option<[f64; 3]> {
2531 if points.len() < 2 {
2532 return None;
2533 }
2534 let steps: Vec<f64> = points
2535 .windows(2)
2536 .map(|pair| dot(sub(pair[1], pair[0]), sub(pair[1], pair[0])).sqrt())
2537 .collect();
2538 let total: f64 = steps.iter().sum();
2539 if !(total > 0.0) {
2540 return Some(points[0]);
2541 }
2542 let target = total * fraction.clamp(0.0, 1.0);
2543 let mut walked = 0.0;
2544 for (index, step) in steps.iter().enumerate() {
2545 if walked + step >= target || index + 1 == steps.len() {
2546 let t = if *step > 0.0 { ((target - walked) / step).clamp(0.0, 1.0) } else { 0.0 };
2547 let (a, b) = (points[index], points[index + 1]);
2548 return Some([
2549 a[0] + (b[0] - a[0]) * t,
2550 a[1] + (b[1] - a[1]) * t,
2551 a[2] + (b[2] - a[2]) * t,
2552 ]);
2553 }
2554 walked += step;
2555 }
2556 points.last().copied()
2557}
2558
2559fn fit_circle(points: &[[f64; 3]]) -> Option<([f64; 3], [f64; 3], f64)> {
2568 if points.len() < 4 {
2569 return None;
2570 }
2571 let last = points.len() - 1;
2572 let (a, b, c) = (points[0], points[last / 3], points[last * 2 / 3]);
2573 let (ab, ac) = (sub(b, a), sub(c, a));
2574 let n = cross(ab, ac);
2575 let n2 = dot(n, n);
2576 if n2 < 1e-24 {
2577 return None;
2578 }
2579 let centre = {
2581 let (ab2, ac2) = (dot(ab, ab), dot(ac, ac));
2582 let to = cross(
2583 [
2584 ab2 * ac[0] - ac2 * ab[0],
2585 ab2 * ac[1] - ac2 * ab[1],
2586 ab2 * ac[2] - ac2 * ab[2],
2587 ],
2588 n,
2589 );
2590 [
2591 a[0] + to[0] / (2.0 * n2),
2592 a[1] + to[1] / (2.0 * n2),
2593 a[2] + to[2] / (2.0 * n2),
2594 ]
2595 };
2596 let axis = norm(n);
2597 let radius = dot(sub(a, centre), sub(a, centre)).sqrt();
2598 if !(radius > 1e-12) {
2599 return None;
2600 }
2601 let bar = radius * CIRCLE_FIT_TOLERANCE;
2602 for p in points {
2603 let d = sub(*p, centre);
2604 if (dot(d, d).sqrt() - radius).abs() > bar || dot(d, axis).abs() > bar {
2605 return None;
2606 }
2607 }
2608 let first = norm(sub(points[0], centre));
2611 let mut span = 0.0;
2612 for p in points {
2613 let d = norm(sub(*p, centre));
2614 let angle = dot(d, first).clamp(-1.0, 1.0).acos();
2615 span = f64::max(span, angle);
2616 }
2617 if span.to_degrees() < CIRCLE_MIN_SPAN_DEG {
2618 return None;
2619 }
2620 Some((centre, axis, radius))
2621}
2622
2623#[derive(Debug, Clone, Copy)]
2632struct Box2 {
2633 centre: [f64; 2],
2634 right: [f64; 2],
2635 up: [f64; 2],
2636 half: [f64; 2],
2637}
2638
2639impl Box2 {
2640 fn local(&self, p: [f64; 2]) -> Option<[f64; 2]> {
2644 let det = self.right[0] * self.up[1] - self.right[1] * self.up[0];
2645 if det.abs() < 1e-12 {
2646 return None;
2647 }
2648 let d = [p[0] - self.centre[0], p[1] - self.centre[1]];
2649 Some([
2650 (d[0] * self.up[1] - d[1] * self.up[0]) / det,
2651 (d[1] * self.right[0] - d[0] * self.right[1]) / det,
2652 ])
2653 }
2654
2655 fn of_text(run: &SheetText) -> Box2 {
2658 let rows = run.text.lines().count().max(1) as f64;
2659 Box2 {
2660 centre: run.anchor,
2661 right: run.right,
2662 up: run.up,
2663 half: [text_half_width(&run.text, 1.0), rows * 0.5 + TEXT_PAD],
2664 }
2665 }
2666}
2667
2668pub(super) fn text_half_width(text: &str, height: f64) -> f64 {
2673 let columns = text.lines().map(|line| line.chars().count()).max().unwrap_or(0) as f64;
2674 (columns * TEXT_ADVANCE * 0.5 + TEXT_PAD) * height
2675}
2676
2677fn break_for_text(lines: Vec<Vec<[f64; 2]>>, boxes: &[Box2]) -> Vec<Vec<[f64; 2]>> {
2690 if boxes.is_empty() {
2691 return lines;
2692 }
2693 let mut out: Vec<Vec<[f64; 2]>> = Vec::new();
2694 for line in lines {
2695 let mut current: Vec<[f64; 2]> = Vec::new();
2697 for pair in line.windows(2) {
2698 let (p, q) = (pair[0], pair[1]);
2699 let mut kept = visible_spans(p, q, boxes);
2700 if kept.is_empty() {
2701 if current.len() >= 2 {
2702 out.push(std::mem::take(&mut current));
2703 } else {
2704 current.clear();
2705 }
2706 continue;
2707 }
2708 let full = kept.len() == 1 && kept[0].0 <= 0.0 && kept[0].1 >= 1.0;
2709 let at = |t: f64| [p[0] + (q[0] - p[0]) * t, p[1] + (q[1] - p[1]) * t];
2710 if full {
2711 if current.is_empty() {
2712 current.push(p);
2713 }
2714 current.push(q);
2715 continue;
2716 }
2717 if !kept.is_empty() && kept[0].0 <= 0.0 {
2720 let (_, end) = kept.remove(0);
2721 if current.is_empty() {
2722 current.push(p);
2723 }
2724 current.push(at(end));
2725 }
2726 if current.len() >= 2 {
2727 out.push(std::mem::take(&mut current));
2728 } else {
2729 current.clear();
2730 }
2731 for (start, end) in kept {
2732 if end >= 1.0 {
2733 current = vec![at(start), q];
2734 } else {
2735 out.push(vec![at(start), at(end)]);
2736 }
2737 }
2738 }
2739 if current.len() >= 2 {
2740 out.push(current);
2741 }
2742 }
2743 out
2744}
2745
2746pub(super) fn break_for_text_boxes(
2750 lines: Vec<Vec<[f64; 2]>>,
2751 runs: &[SheetText],
2752) -> Vec<Vec<[f64; 2]>> {
2753 let boxes: Vec<Box2> = runs.iter().map(Box2::of_text).collect();
2754 break_for_text(lines, &boxes)
2755}
2756
2757fn visible_spans(p: [f64; 2], q: [f64; 2], boxes: &[Box2]) -> Vec<(f64, f64)> {
2760 let mut blocked: Vec<(f64, f64)> = Vec::new();
2761 for box2 in boxes {
2762 let (Some(a), Some(b)) = (box2.local(p), box2.local(q)) else {
2763 continue;
2764 };
2765 let (mut t0, mut t1) = (0.0f64, 1.0f64);
2767 let mut inside = true;
2768 for (axis, half) in [(0usize, box2.half[0]), (1usize, box2.half[1])] {
2769 let delta = b[axis] - a[axis];
2770 if delta.abs() < 1e-12 {
2771 if a[axis].abs() > half {
2772 inside = false;
2773 break;
2774 }
2775 continue;
2776 }
2777 let (lo, hi) = ((-half - a[axis]) / delta, (half - a[axis]) / delta);
2778 let (lo, hi) = if lo <= hi { (lo, hi) } else { (hi, lo) };
2779 t0 = t0.max(lo);
2780 t1 = t1.min(hi);
2781 if t0 > t1 {
2782 inside = false;
2783 break;
2784 }
2785 }
2786 if inside && t1 > t0 {
2787 blocked.push((t0, t1));
2788 }
2789 }
2790 if blocked.is_empty() {
2791 return vec![(0.0, 1.0)];
2792 }
2793 blocked.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
2794 let mut spans = Vec::new();
2795 let mut cursor = 0.0f64;
2796 for (start, end) in blocked {
2797 if start > cursor + 1e-9 {
2798 spans.push((cursor, start.min(1.0)));
2799 }
2800 cursor = cursor.max(end);
2801 if cursor >= 1.0 {
2802 break;
2803 }
2804 }
2805 if cursor < 1.0 - 1e-9 {
2806 spans.push((cursor, 1.0));
2807 }
2808 spans
2809}
2810
2811pub fn plane_is_edge_on(row: &PmiAnnotationReport, forward: [f64; 3]) -> bool {
2816 match row.plane.as_ref() {
2817 None => false,
2818 Some(plane) => dot(norm(plane.normal), forward).abs() < EDGE_ON_COS,
2819 }
2820}
2821
2822fn tolerance_sources(
2828 report: Option<&PmiViewReport>,
2829 view: &PmiView,
2830 frame: &ViewFrame,
2831 placed: &PlacedView,
2832 env: &brep_kernel::Env,
2833) -> Vec<ToleranceSource> {
2834 let Some(report) = report else { return Vec::new() };
2835 let project = |p: [f64; 3]| -> [f64; 2] {
2836 let v = frame.to_view(p);
2837 to_paper(placed, v[0], v[1])
2838 };
2839 let mut out = Vec::new();
2840 for row in report.annotations.iter().filter(|row| row.enabled && row.status == PmiStatus::Ok) {
2841 let Some(annotation) = view.annotations.iter().find(|annotation| annotation.id() == row.id) else { continue };
2842 let Ok(block) = brep_kernel::ToleranceBlock::read(annotation, env) else { continue };
2843 let reference = annotation.flag("isReference");
2844 if block.mode == brep_kernel::ToleranceMode::None && !reference {
2845 continue;
2846 }
2847 let measures = match &row.geometry {
2848 brep_kernel::PmiGeometry::Linear { a, b, component: None } => Measures::Linear { a: project(*a), b: project(*b) },
2849 brep_kernel::PmiGeometry::Radial { center, radius, diameter, sphere: false, .. } => {
2850 Measures::Radial { centre: project(*center), radius: *radius, diameter: *diameter }
2851 }
2852 _ => continue,
2853 };
2854 out.push(ToleranceSource { id: row.id.clone(), measures, block, reference });
2855 }
2856 out
2857}
2858
2859fn annotations(
2860 report: Option<&PmiViewReport>,
2861 view: &PmiView,
2862 frame: &ViewFrame,
2863 placed: &PlacedView,
2864) -> Vec<AnnotationDrawing> {
2865 let Some(report) = report else {
2866 return Vec::new();
2867 };
2868 let paper_text = view.display.text_size_pt * MM_PER_POINT;
2872 let text_height = paper_text / placed.scale.max(1e-9);
2873 let style = PmiLayoutStyle {
2874 arrow: text_height * ARROW_PER_TEXT,
2875 text_height,
2876 view_dir: frame.forward,
2877 view_up: frame.up,
2878 plane: None,
2879 };
2880 let flatten = placed.flatten_text;
2881 let mut out = Vec::new();
2882 for row in &report.annotations {
2883 if !row.enabled || row.status != PmiStatus::Ok {
2884 continue;
2885 }
2886 let row_style = PmiLayoutStyle { plane: row.plane, ..style };
2887 let drawn = pmi_present(&row.geometry, row.label_world, &row.text, &row_style);
2888 let project = |p: [f64; 3]| -> [f64; 2] {
2891 let v = frame.to_view(p);
2892 to_paper(placed, v[0], v[1])
2893 };
2894 let project_line = |polyline: &Vec<[f64; 3]>| -> Vec<[f64; 2]> {
2895 polyline.iter().map(|p| project(*p)).collect()
2896 };
2897 let mut lines: Vec<Vec<[f64; 2]>> = drawn.polylines.iter().map(project_line).collect();
2898 for arrow in &drawn.arrows {
2901 let mut closed: Vec<[f64; 3]> = arrow.to_vec();
2902 closed.push(arrow[0]);
2903 lines.push(project_line(&closed));
2904 }
2905
2906 let unit = drawn
2913 .texts
2914 .first()
2915 .filter(|_| !drawn.frames.is_empty())
2916 .map(|run| Unit {
2917 anchor: drawn.text_anchor,
2918 right: run.right,
2919 up: run.up,
2920 cap: run.height,
2921 });
2922 let flat_about = |anchor: [f64; 3], right3: [f64; 3], up3: [f64; 3], p: [f64; 3]| -> [f64; 2] {
2927 let d = sub(p, anchor);
2928 let a = project(anchor);
2929 [a[0] + dot(d, right3) * placed.scale, a[1] - dot(d, up3) * placed.scale]
2930 };
2931
2932 let explode = matches!(row.geometry, brep_kernel::PmiGeometry::Explode { .. } | brep_kernel::PmiGeometry::Hide { .. });
2945 let texts: Vec<SheetText> = drawn
2946 .texts
2947 .iter()
2948 .filter(|run| !explode && !run.text.trim().is_empty())
2949 .map(|run| {
2950 let cap = run.height * placed.scale;
2951 if flatten {
2952 let anchor = match unit {
2956 Some(unit) => flat_about(unit.anchor, unit.right, unit.up, run.anchor),
2957 None => project(run.anchor),
2958 };
2959 return SheetText::flat(run.text.clone(), anchor, cap);
2960 }
2961 let image = |v: [f64; 3]| -> [f64; 2] {
2963 [dot(v, frame.right) * cap, -dot(v, frame.up) * cap]
2964 };
2965 let mut right = image(run.right);
2966 if dot(cross(run.right, run.up), frame.forward) > 0.0 {
2971 right = [-right[0], -right[1]];
2972 }
2973 SheetText::new(run.text.clone(), project(run.anchor), right, image(run.up))
2974 })
2975 .collect();
2976
2977 let frames: Vec<Vec<[f64; 2]>> = match (flatten, unit) {
2980 (true, Some(unit)) => drawn
2981 .frames
2982 .iter()
2983 .map(|polyline| {
2984 polyline
2985 .iter()
2986 .map(|p| flat_about(unit.anchor, unit.right, unit.up, *p))
2987 .collect()
2988 })
2989 .collect(),
2990 _ => drawn.frames.iter().map(project_line).collect(),
2991 };
2992 let mut boxes: Vec<Box2> = texts.iter().map(Box2::of_text).collect();
2996 if let Some(unit) = unit {
2997 let scaled = unit.cap * placed.scale;
2998 let (right, up) = if flatten {
2999 ([scaled, 0.0], [0.0, -scaled])
3000 } else {
3001 let image = |v: [f64; 3]| [dot(v, frame.right) * scaled, -dot(v, frame.up) * scaled];
3002 (image(unit.right), image(unit.up))
3003 };
3004 if let Some(box2) = unit.box_of(&drawn.frames, project(unit.anchor), right, up) {
3005 boxes.push(box2);
3006 }
3007 }
3008 let mut lines = break_for_text(lines, &boxes);
3009 lines.extend(frames);
3010
3011 out.push(AnnotationDrawing {
3012 id: row.id.clone(),
3013 kind: row.kind.clone(),
3014 text: row.text.clone(),
3015 lines,
3016 texts,
3017 note: if !plane_is_edge_on(row, frame.forward) {
3018 String::new()
3019 } else if flatten {
3020 format!(
3021 "annotation plane edge-on to '{}': its geometry projects to a line, its text is flattened onto the sheet",
3022 view.name
3023 )
3024 } else {
3025 format!(
3026 "annotation plane edge-on to '{}': it and its text project to a line",
3027 view.name
3028 )
3029 },
3030 });
3031 }
3032 out
3033}
3034
3035#[derive(Debug, Clone, Copy)]
3041struct Unit {
3042 anchor: [f64; 3],
3043 right: [f64; 3],
3044 up: [f64; 3],
3045 cap: f64,
3046}
3047
3048impl Unit {
3049 fn box_of(
3055 &self,
3056 frames: &[Vec<[f64; 3]>],
3057 anchor: [f64; 2],
3058 right: [f64; 2],
3059 up: [f64; 2],
3060 ) -> Option<Box2> {
3061 if self.cap <= 0.0 {
3062 return None;
3063 }
3064 let mut lo = [f64::INFINITY; 2];
3065 let mut hi = [f64::NEG_INFINITY; 2];
3066 for polyline in frames {
3067 for p in polyline {
3068 let d = sub(*p, self.anchor);
3069 let local = [dot(d, self.right) / self.cap, dot(d, self.up) / self.cap];
3070 for axis in 0..2 {
3071 lo[axis] = lo[axis].min(local[axis]);
3072 hi[axis] = hi[axis].max(local[axis]);
3073 }
3074 }
3075 }
3076 if lo[0] > hi[0] {
3077 return None;
3078 }
3079 let mid = [(lo[0] + hi[0]) * 0.5, (lo[1] + hi[1]) * 0.5];
3080 Some(Box2 {
3081 centre: [
3082 anchor[0] + mid[0] * right[0] + mid[1] * up[0],
3083 anchor[1] + mid[0] * right[1] + mid[1] * up[1],
3084 ],
3085 right,
3086 up,
3087 half: [(hi[0] - lo[0]) * 0.5, (hi[1] - lo[1]) * 0.5],
3088 })
3089 }
3090}
3091
3092
3093
3094