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 #[serde(skip_serializing_if = "Vec::is_empty")]
150 pub highlighted: Vec<Vec<[f64; 2]>>,
151 pub annotations: Vec<AnnotationDrawing>,
153 pub anchors: Vec<AnchorPoint>,
159 #[serde(rename = "textMm")]
163 pub text_mm: f64,
164 #[serde(skip_serializing_if = "Option::is_none")]
167 pub section: Option<SectionDrawing>,
168 #[serde(rename = "sectionMarks", skip_serializing_if = "Vec::is_empty")]
171 pub section_marks: Vec<SectionMark>,
172 #[serde(skip_serializing_if = "Option::is_none")]
175 pub detail: Option<DetailDrawing>,
176 #[serde(rename = "detailMarks", skip_serializing_if = "Vec::is_empty")]
179 pub detail_marks: Vec<DetailMark>,
180 pub bounds: [f64; 4],
183 #[serde(rename = "hiddenLine", skip_serializing_if = "String::is_empty")]
189 pub hidden_line: String,
190 #[serde(skip)]
196 pub tolerances: Vec<ToleranceSource>,
197}
198
199#[derive(Debug, Clone, PartialEq)]
202pub struct ToleranceSource {
203 pub id: String,
205 pub measures: Measures,
206 pub block: brep_kernel::ToleranceBlock,
207 pub reference: bool,
208}
209
210#[derive(Debug, Clone, PartialEq)]
212pub enum Measures {
213 Linear { a: [f64; 2], b: [f64; 2] },
217 Radial { centre: [f64; 2], radius: f64, diameter: bool },
220}
221
222impl ViewDrawing {
223 fn empty(placed: &PlacedView, name: String) -> ViewDrawing {
227 ViewDrawing {
228 id: placed.id.clone(),
229 view: placed.view.clone(),
230 name,
231 position: placed.position,
232 scale: placed.scale,
233 error: String::new(),
234 edges: Vec::new(),
235 highlighted: Vec::new(),
236 annotations: Vec::new(),
237 anchors: Vec::new(),
238 text_mm: 0.0,
239 section: None,
240 section_marks: Vec::new(),
241 detail: None,
242 detail_marks: Vec::new(),
243 bounds: [placed.position[0], placed.position[1], placed.position[0], placed.position[1]],
244 hidden_line: String::new(),
245 tolerances: Vec::new(),
246 }
247 }
248}
249
250#[derive(Debug, Clone, PartialEq, serde::Serialize)]
256pub struct DetailDrawing {
257 pub label: String,
258 pub centre: [f64; 2],
260 #[serde(rename = "radiusMm")]
263 pub radius_mm: f64,
264 pub circle: Vec<[f64; 2]>,
266 pub texts: Vec<SheetText>,
268}
269
270#[derive(Debug, Clone, PartialEq, serde::Serialize)]
273pub struct DetailMark {
274 pub id: String,
276 pub label: String,
277 pub centre: [f64; 2],
278 #[serde(rename = "radiusMm")]
279 pub radius_mm: f64,
280 pub lines: Vec<Vec<[f64; 2]>>,
281 pub texts: Vec<SheetText>,
282}
283
284#[derive(Debug, Clone, PartialEq, serde::Serialize)]
292pub struct SectionDrawing {
293 pub label: String,
295 pub hatch: Vec<Vec<[f64; 2]>>,
298 pub cut: Vec<Vec<[f64; 2]>>,
305 pub texts: Vec<SheetText>,
307}
308
309#[derive(Debug, Clone, PartialEq, serde::Serialize)]
313pub struct SectionMark {
314 pub id: String,
316 pub label: String,
317 pub lines: Vec<Vec<[f64; 2]>>,
318 pub texts: Vec<SheetText>,
319}
320
321#[derive(Debug, Clone, PartialEq, serde::Serialize)]
323pub struct AnnotationDrawing {
324 pub id: String,
325 #[serde(rename = "type")]
326 pub kind: String,
327 pub text: String,
330 pub lines: Vec<Vec<[f64; 2]>>,
332 pub texts: Vec<SheetText>,
333 #[serde(skip_serializing_if = "String::is_empty")]
339 pub note: String,
340}
341
342#[derive(Debug, Clone, PartialEq, serde::Serialize)]
351pub struct AnchorPoint {
352 #[serde(rename = "ref")]
353 pub anchor: String,
354 pub kind: String,
356 pub at: [f64; 2],
358 pub model: [f64; 3],
362 #[serde(skip_serializing_if = "Option::is_none")]
373 pub dir: Option<[f64; 2]>,
374 #[serde(skip_serializing_if = "Option::is_none")]
376 pub radius: Option<f64>,
377 #[serde(skip_serializing_if = "Option::is_none")]
381 pub u: Option<[f64; 2]>,
382 #[serde(skip_serializing_if = "Option::is_none")]
383 pub v: Option<[f64; 2]>,
384}
385
386#[derive(Debug, Clone, PartialEq, serde::Serialize)]
404pub struct SheetText {
405 pub text: String,
406 pub anchor: [f64; 2],
407 pub height: f64,
408 pub angle: f64,
409 pub right: [f64; 2],
410 pub up: [f64; 2],
411}
412
413impl SheetText {
414 pub fn new(text: String, anchor: [f64; 2], right: [f64; 2], up: [f64; 2]) -> SheetText {
417 SheetText {
418 text,
419 anchor,
420 height: (up[0] * up[0] + up[1] * up[1]).sqrt(),
421 angle: right[1].atan2(right[0]).to_degrees(),
424 right,
425 up,
426 }
427 }
428
429 pub fn flat(text: String, anchor: [f64; 2], height: f64) -> SheetText {
433 SheetText::new(text, anchor, [height, 0.0], [0.0, -height])
434 }
435}
436
437#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
445pub struct ViewFrame {
446 pub target: [f64; 3],
447 pub right: [f64; 3],
448 pub up: [f64; 3],
449 pub forward: [f64; 3],
450}
451
452fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
453 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
454}
455
456fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
457 [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
458}
459
460fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
461 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
462}
463
464fn norm(v: [f64; 3]) -> [f64; 3] {
465 let len = dot(v, v).sqrt();
466 if len < 1e-12 {
467 [0.0, 0.0, 0.0]
468 } else {
469 [v[0] / len, v[1] / len, v[2] / len]
470 }
471}
472
473impl ViewFrame {
474 pub fn of(camera: &PmiCamera) -> ViewFrame {
478 let forward = norm(sub(camera.target, camera.eye));
479 let forward = if forward == [0.0, 0.0, 0.0] { [0.0, 0.0, -1.0] } else { forward };
480 let mut right = norm(cross(forward, camera.up));
481 if right == [0.0, 0.0, 0.0] {
482 right = norm(cross(forward, [0.0, 0.0, 1.0]));
485 if right == [0.0, 0.0, 0.0] {
486 right = norm(cross(forward, [0.0, 1.0, 0.0]));
487 }
488 }
489 let up = cross(right, forward);
490 ViewFrame { target: camera.target, right, up, forward }
491 }
492
493 pub fn to_view(&self, p: [f64; 3]) -> [f64; 3] {
496 let d = sub(p, self.target);
497 [dot(d, self.right), dot(d, self.up), dot(d, self.forward)]
498 }
499}
500
501fn to_paper(placed: &PlacedView, x: f64, y: f64) -> [f64; 2] {
503 [placed.position[0] + x * placed.scale, placed.position[1] - y * placed.scale]
504}
505
506pub fn project_view(
514 scene: &RenderScene,
515 placed: &PlacedView,
516 view: Option<&PmiView>,
517 report: Option<&PmiViewReport>,
518) -> ViewDrawing {
519 let (unposed, env) = (Unposed::new(), brep_kernel::Env::default());
520 project_view_through(&Model { scene, unposed: &unposed, env: &env }, placed, view, report, None, None, &mut LinesMode::Compute)
521}
522
523pub struct Model<'a> {
529 pub scene: &'a RenderScene,
530 pub unposed: &'a Unposed,
531 pub env: &'a brep_kernel::Env,
532}
533
534pub type Unposed = std::collections::HashMap<String, crate::scene::SolidDisplay>;
538
539#[derive(Debug, Clone, Copy, PartialEq, Eq)]
541pub enum HiddenLine {
542 Analytic,
544 Mesh,
547}
548
549impl HiddenLine {
550 pub fn label(self) -> &'static str {
551 match self {
552 HiddenLine::Analytic => "analytic",
553 HiddenLine::Mesh => "mesh",
554 }
555 }
556}
557
558pub struct SectionSetup {
562 pub frame: ViewFrame,
563 pub plane: ClipPlane,
564 pub half: Option<super::hlr::HalfLine>,
565 pub label: String,
566}
567
568pub struct DetailSetup {
573 pub frame: ViewFrame,
574 pub radius: f64,
575 pub label: String,
576}
577
578pub enum Derived {
581 Section(SectionSetup),
582 Detail(DetailSetup),
583}
584
585pub fn project_view_through(
606 model: &Model,
607 placed: &PlacedView,
608 view: Option<&PmiView>,
609 report: Option<&PmiViewReport>,
610 derived: Option<Derived>,
611 letter: Option<&str>,
612 mode: &mut LinesMode,
613) -> ViewDrawing {
614 let (scene, unposed) = (model.scene, model.unposed);
615 let mut out = ViewDrawing::empty(placed, view.map(|v| v.name.clone()).unwrap_or_default());
616 let Some(view) = view else {
617 out.error = format!("no PMI view '{}'", placed.view);
618 return out;
619 };
620 let Some(camera) = view.camera.as_ref() else {
621 out.error = format!("PMI view '{}' has no captured camera", placed.view);
622 return out;
623 };
624 if !matches!(camera.projection, PmiProjection::Orthographic { .. }) {
625 out.error = format!(
626 "PMI view '{}' is a perspective camera; a sheet draws {ORTHOGRAPHIC} views only",
627 placed.view
628 );
629 return out;
630 }
631 if !placed.projection.eq_ignore_ascii_case(ORTHOGRAPHIC) {
632 out.error = format!("projection '{}' is not drawn on a sheet", placed.projection);
633 return out;
634 }
635
636 let saved_section = derived.is_none() && view.display.section.is_some();
637 let planes = if derived.is_none() { view.section_planes_in(report) } else { Vec::new() };
640 if planes.len() > 2 || planes.get(1).is_some_and(|(_, second)| dot(*second, planes[0].1).abs() > 1e-6) {
641 out.error = format!(
642 "PMI view '{}' is sectioned by {} planes; a sheet draws one plane, or a half section of two perpendicular planes",
643 placed.view,
644 planes.len()
645 );
646 return out;
647 }
648 let derived = derived.or_else(|| planes.first().map(|&(point, normal)| {
649 let original = ViewFrame::of(camera);
650 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.] };
651 let right = norm(cross(normal, seed));
652 let frame = ViewFrame { target: point, right, up: cross(right, normal), forward: normal };
653 let half = planes.get(1).map(|&(at, second)| {
654 let (normal, at) = ([dot(second, frame.right), dot(second, frame.up)], frame.to_view(at));
655 super::hlr::HalfLine { normal, offset: normal[0] * at[0] + normal[1] * at[1] }
656 });
657 Derived::Section(SectionSetup { frame, plane: ClipPlane { point, normal }, half, label: letter.unwrap_or("A").to_string() })
658 }));
659 let frame = match &derived {
660 Some(Derived::Section(setup)) => setup.frame,
661 Some(Derived::Detail(setup)) => setup.frame,
662 None => ViewFrame::of(camera),
663 };
664 out.text_mm = view.display.text_size_pt * MM_PER_POINT;
665 let cap = if out.text_mm > 1e-9 { out.text_mm } else { 12.0 * MM_PER_POINT };
666 let plan = exact_solids(scene, unposed, view, report);
671 let clip = match &derived {
672 None => LinesClip::Whole,
673 Some(Derived::Detail(setup)) => LinesClip::Circle { radius: setup.radius },
674 Some(Derived::Section(setup)) => LinesClip::Section { half: setup.half },
675 };
676 let exact = plan.as_ref().and_then(|plan| exact_lines_for(plan, &frame, placed, clip, mode));
677 if matches!(mode, LinesMode::Export(_)) && exact.is_none() {
678 if plan.is_none() {
679 out.error = "exact geometry is unavailable for this picture".into();
680 }
681 return out;
682 }
683 out.hidden_line = if exact.is_some() { HiddenLine::Analytic } else { HiddenLine::Mesh }.label().into();
684 match &derived {
685 None => {
686 out.edges = match exact {
687 Some(lines) => lines.runs,
688 None => mesh_lines_for(scene, unposed, view, report, &frame, placed, LinesClip::Whole, None).runs,
689 };
690 out.annotations = annotations(report, view, &frame, placed);
691 out.tolerances = tolerance_sources(report, view, &frame, placed, model.env);
692 }
693 Some(Derived::Detail(setup)) => {
694 let radius_mm = setup.radius * placed.scale;
695 let lines = match exact {
699 Some(lines) => lines.runs,
700 None => mesh_lines_for(scene, unposed, view, report, &frame, placed, clip, None).runs,
701 };
702 out.edges = clip_to_circle(lines, placed.position, radius_mm);
703 out.detail = Some(DetailDrawing {
704 label: setup.label.clone(),
705 centre: placed.position,
706 radius_mm,
707 circle: circle_polyline(placed.position, radius_mm),
708 texts: Vec::new(),
709 });
710 }
711 Some(Derived::Section(setup)) => match exact {
712 Some(ExactLines { runs, cut }) => {
713 let mut edges = runs;
716 edges.extend(cut.iter().cloned());
717 out.edges = edges;
718 out.section = Some(SectionDrawing {
719 label: setup.label.clone(),
720 hatch: hatch(&segments_of(&cut)),
721 cut,
722 texts: Vec::new(),
723 });
724 }
725 None => {
726 let ExactLines { runs, cut: cut_paper } =
727 mesh_lines_for(scene, unposed, view, report, &frame, placed, clip, Some(&setup.plane));
728 let mut edges = runs;
729 edges.extend(cut_paper.iter().cloned());
730 out.edges = edges;
731 out.section = Some(SectionDrawing {
732 label: setup.label.clone(),
733 hatch: hatch(&cut_paper),
734 cut: cut_paper,
735 texts: Vec::new(),
736 });
737 }
738 },
739 }
740 if saved_section {
741 out.annotations = annotations(report, view, &frame, placed);
742 out.tolerances = tolerance_sources(report, view, &frame, placed, model.env);
743 }
744 out.anchors = anchor_points(scene, unposed, view, report, &frame, placed);
745 if let Some(detail) = &out.detail {
752 let reach = detail.radius_mm * (1.0 + 1e-9) + 1e-9;
753 out.anchors.retain(|anchor| {
754 let (dx, dy) = (anchor.at[0] - detail.centre[0], anchor.at[1] - detail.centre[1]);
755 (dx * dx + dy * dy).sqrt() <= reach
756 });
757 }
758 out.bounds = bounds_of(&out, placed);
759 let middle = (out.bounds[0] + out.bounds[2]) * 0.5;
763 let below = out.bounds[3] + cap * 2.0;
764 if let Some(section) = out.section.as_mut() {
765 section.texts.push(SheetText::flat(
766 format!("SECTION {}\u{2014}{}", section.label, section.label),
767 [middle, below],
768 cap,
769 ));
770 out.bounds = bounds_of(&out, placed);
771 }
772 if let Some(detail) = out.detail.as_mut() {
775 detail.texts.push(SheetText::flat(format!("DETAIL {}", detail.label), [middle, below], cap));
776 detail.texts.push(SheetText::flat(
777 format!("SCALE {}", super::frame::ratio_text(placed.scale)),
778 [middle, below + cap * 1.8],
779 cap * 0.8,
780 ));
781 out.bounds = bounds_of(&out, placed);
782 }
783 out
784}
785
786pub fn project_sheet(
792 scene: &RenderScene,
793 sheet: &super::Sheet,
794 views: &[PmiView],
795 report: Option<&brep_kernel::PmiReport>,
796 context: &super::frame::FrameContext,
797) -> SheetDrawing {
798 let (unposed, env) = (Unposed::new(), brep_kernel::Env::default());
799 project_sheet_with(&Model { scene, unposed: &unposed, env: &env }, sheet, views, report, context, &mut LinesMode::Compute)
800}
801
802pub fn project_sheet_with(
806 model: &Model,
807 sheet: &super::Sheet,
808 views: &[PmiView],
809 report: Option<&brep_kernel::PmiReport>,
810 context: &super::frame::FrameContext,
811 mode: &mut LinesMode,
812) -> SheetDrawing {
813 let (width_mm, height_mm) = sheet.millimetres();
814 let letters = sheet.view_letters(views);
822 let mut drawn: Vec<ViewDrawing> = sheet
823 .views
824 .iter()
825 .zip(&letters)
826 .map(|(placed, letter)| {
827 let saved = views.iter().find(|view| view.id == placed.view);
828 if placed.section.is_some() || placed.detail.is_some() {
829 ViewDrawing::empty(placed, saved.map(|v| v.name.clone()).unwrap_or_default())
833 } else {
834 project_view_through(model, placed, saved, report.and_then(|report| report.view(&placed.view)), None, letter.as_deref(), mode)
835 }
836 })
837 .collect();
838 for (index, placed) in sheet.views.iter().enumerate() {
839 let Some(circle) = placed.detail.as_ref() else { continue };
840 match detail_setup(sheet, &drawn, circle, views) {
841 Err(error) => drawn[index].error = error,
842 Ok((setup, source, mark)) => {
843 drawn[index] = project_view_through(
844 model,
845 placed,
846 views.iter().find(|view| view.id == placed.view),
847 report.and_then(|report| report.view(&placed.view)),
848 Some(Derived::Detail(setup)),
849 None,
850 mode,
851 );
852 let mark = DetailMark { id: placed.id.clone(), ..mark };
853 drawn[source].detail_marks.push(mark);
854 let source_placed = &sheet.views[source];
855 drawn[source].bounds = bounds_of(&drawn[source], source_placed);
856 }
857 }
858 }
859 for (index, placed) in sheet.views.iter().enumerate() {
860 let Some(cut) = placed.section.as_ref() else { continue };
861 match section_setup(sheet, &drawn, placed, cut, views) {
862 Err(error) => drawn[index].error = error,
863 Ok((setup, source, mark)) => {
864 drawn[index] = project_view_through(
865 model,
866 placed,
867 views.iter().find(|view| view.id == placed.view),
868 report.and_then(|report| report.view(&placed.view)),
869 Some(Derived::Section(setup)),
870 None,
871 mode,
872 );
873 drawn[source].section_marks.push(mark);
877 let source_placed = &sheet.views[source];
878 drawn[source].bounds = bounds_of(&drawn[source], source_placed);
879 }
880 }
881 }
882 for (index, placed) in sheet.views.iter().enumerate() {
885 if placed.section.is_some() || placed.detail.is_some() {
886 continue;
887 }
888 let Some(label) = drawn[index].section.as_ref().map(|section| section.label.clone()) else { continue };
889 let Some(view) = views.iter().find(|view| view.id == placed.view) else { continue };
890 let Some((point, normal)) = view.section_plane_in(report.and_then(|report| report.view(&placed.view))) else { continue };
891 for (other, source) in sheet.views.iter().enumerate() {
892 if other == index || source.section.is_some() || source.detail.is_some() {
893 continue;
894 }
895 if drawn[other].section.is_some() || !drawn[other].error.is_empty() {
896 continue;
897 }
898 let Some(camera) = views.iter().find(|view| view.id == source.view).and_then(|view| view.camera.as_ref()) else { continue };
899 let frame = ViewFrame::of(camera);
900 if dot(normal, frame.forward).abs() > 1e-6 {
901 continue;
902 }
903 let Some(mark) = plane_mark(&drawn[other], source, &frame, point, normal, &placed.id, &label) else { continue };
904 drawn[other].section_marks.push(mark);
905 drawn[other].bounds = bounds_of(&drawn[other], source);
906 }
907 }
908 let dimensions = super::dimension::layout_dimensions(
912 &sheet.dimensions,
913 &drawn,
914 [width_mm * 0.5, height_mm * 0.5],
915 );
916 let ordinates = super::ordinate::layout_ordinates(
917 &sheet.ordinates,
918 &drawn,
919 [width_mm * 0.5, height_mm * 0.5],
920 );
921 SheetDrawing {
922 id: sheet.id.clone(),
923 name: sheet.name.clone(),
924 width_mm,
925 height_mm,
926 frame: super::frame::frame(sheet, context),
927 views: drawn,
928 dimensions,
929 ordinates,
930 }
931}
932
933fn section_setup(
960 sheet: &super::Sheet,
961 drawn: &[ViewDrawing],
962 placed: &PlacedView,
963 cut: &super::SectionCut,
964 views: &[PmiView],
965) -> Result<(SectionSetup, usize, SectionMark), String> {
966 if cut.from.is_empty() || cut.to.is_empty() {
969 return Err("no cutting line yet \u{2014} pick its two anchors on a placed view".into());
970 }
971 let Some(source_index) = sheet.views.iter().position(|v| v.id == cut.source) else {
972 return Err(format!("no placement '{}' to cut through on this sheet", cut.source));
973 };
974 let source_placed = &sheet.views[source_index];
975 if source_placed.section.is_some() {
976 return Err(format!(
977 "'{}' is itself a section; a section of a section is not drawn",
978 cut.source
979 ));
980 }
981 if source_placed.detail.is_some() {
982 return Err(format!(
983 "'{}' is a detail; a section of a detail is not drawn",
984 cut.source
985 ));
986 }
987 let source = &drawn[source_index];
988 if !source.error.is_empty() {
989 return Err(format!("placement '{}' did not project", cut.source));
990 }
991 let (from, to) = (point_of(source, &cut.from)?, point_of(source, &cut.to)?);
992 let span = [to[0] - from[0], to[1] - from[1]];
993 let length = (span[0] * span[0] + span[1] * span[1]).sqrt();
994 if !(length > 1e-9) {
995 return Err(format!(
996 "'{}' and '{}' are the same point; a cutting line needs two",
997 cut.from, cut.to
998 ));
999 }
1000 let Some(camera) = views
1001 .iter()
1002 .find(|view| view.id == source_placed.view)
1003 .and_then(|view| view.camera.as_ref())
1004 else {
1005 return Err(format!("placement '{}' has no captured camera to cut in", cut.source));
1006 };
1007 let source_frame = ViewFrame::of(camera);
1008 if source_placed.scale <= 0.0 {
1009 return Err(format!("placement '{}' has no scale", cut.source));
1010 }
1011 let (p0, p1) = (lift(&source_frame, source_placed, from), lift(&source_frame, source_placed, to));
1016 let along = norm(sub(p1, p0));
1017 let mut forward = norm(cross(source_frame.forward, along));
1018 if forward == [0.0, 0.0, 0.0] {
1019 return Err("the cutting line has no direction in this view".into());
1020 }
1021 if cut.flip {
1022 forward = [-forward[0], -forward[1], -forward[2]];
1023 }
1024 let up = [-source_frame.forward[0], -source_frame.forward[1], -source_frame.forward[2]];
1025 let right = norm(cross(forward, up));
1026 let target = [(p0[0] + p1[0]) * 0.5, (p0[1] + p1[1]) * 0.5, (p0[2] + p1[2]) * 0.5];
1027 let setup = SectionSetup {
1028 frame: ViewFrame { target, right, up: cross(right, forward), forward },
1029 plane: ClipPlane { point: target, normal: forward },
1030 half: None,
1031 label: cut.label.clone(),
1032 };
1033 let cap = if source.text_mm > 1e-9 { source.text_mm } else { 12.0 * MM_PER_POINT };
1034 Ok((setup, source_index, section_mark(placed, cut, from, to, cap)))
1035}
1036
1037fn section_mark(
1041 placed: &PlacedView,
1042 cut: &super::SectionCut,
1043 from: [f64; 2],
1044 to: [f64; 2],
1045 cap: f64,
1046) -> SectionMark {
1047 let span = [to[0] - from[0], to[1] - from[1]];
1048 let length = (span[0] * span[0] + span[1] * span[1]).sqrt().max(1e-9);
1049 let along = [span[0] / length, span[1] / length];
1050 let sign = if cut.flip { -1.0 } else { 1.0 };
1055 let look = [-along[1] * sign, along[0] * sign];
1056 cutting_mark(&placed.id, &cut.label, from, to, look, cap)
1057}
1058
1059fn plane_mark(
1064 source: &ViewDrawing,
1065 placed: &PlacedView,
1066 frame: &ViewFrame,
1067 point: [f64; 3],
1068 normal: [f64; 3],
1069 id: &str,
1070 label: &str,
1071) -> Option<SectionMark> {
1072 let paper = |v: [f64; 3]| {
1073 let length = (v[0] * v[0] + v[1] * v[1]).sqrt();
1074 (length > 1e-9).then(|| [v[0] / length, v[1] / length])
1075 };
1076 let at = frame.to_view(point);
1078 let centre = to_paper(placed, at[0], at[1]);
1079 let trace = cross(normal, frame.forward);
1080 let along = paper([dot(trace, frame.right), -dot(trace, frame.up), 0.0])?;
1081 let look = paper([dot(normal, frame.right), -dot(normal, frame.up), 0.0])?;
1082 let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
1083 for p in source.edges.iter().flatten() {
1084 let t = (p[0] - centre[0]) * along[0] + (p[1] - centre[1]) * along[1];
1085 lo = lo.min(t);
1086 hi = hi.max(t);
1087 }
1088 if !(hi > lo) {
1089 return None;
1090 }
1091 let from = [centre[0] + along[0] * lo, centre[1] + along[1] * lo];
1092 let to = [centre[0] + along[0] * hi, centre[1] + along[1] * hi];
1093 let cap = if source.text_mm > 1e-9 { source.text_mm } else { 12.0 * MM_PER_POINT };
1094 Some(cutting_mark(id, label, from, to, look, cap))
1095}
1096
1097fn cutting_mark(id: &str, label: &str, from: [f64; 2], to: [f64; 2], look: [f64; 2], cap: f64) -> SectionMark {
1101 let span = [to[0] - from[0], to[1] - from[1]];
1102 let length = (span[0] * span[0] + span[1] * span[1]).sqrt().max(1e-9);
1103 let along = [span[0] / length, span[1] / length];
1104 let tail = cap * 1.5;
1105 let arrow = cap * 1.2;
1106 let mut lines = vec![vec![
1107 [from[0] - along[0] * tail, from[1] - along[1] * tail],
1108 [to[0] + along[0] * tail, to[1] + along[1] * tail],
1109 ]];
1110 let mut texts = Vec::new();
1111 for (end, outward) in [(from, [-along[0], -along[1]]), (to, along)] {
1112 let root = [end[0] + outward[0] * tail, end[1] + outward[1] * tail];
1113 let tip = [root[0] + look[0] * arrow * 2.0, root[1] + look[1] * arrow * 2.0];
1114 lines.push(vec![root, tip]);
1118 let across = [-look[1] * arrow * 0.35, look[0] * arrow * 0.35];
1119 lines.push(vec![
1120 tip,
1121 [tip[0] - look[0] * arrow + across[0], tip[1] - look[1] * arrow + across[1]],
1122 [tip[0] - look[0] * arrow - across[0], tip[1] - look[1] * arrow - across[1]],
1123 tip,
1124 ]);
1125 texts.push(SheetText::flat(
1126 label.to_string(),
1127 [
1128 root[0] + outward[0] * cap * 1.2 + look[0] * cap * 0.6,
1129 root[1] + outward[1] * cap * 1.2 + look[1] * cap * 0.6,
1130 ],
1131 cap,
1132 ));
1133 }
1134 SectionMark { id: id.to_string(), label: label.to_string(), lines, texts }
1135}
1136
1137fn point_of(source: &ViewDrawing, reference: &str) -> Result<[f64; 2], String> {
1140 source
1141 .anchors
1142 .iter()
1143 .find(|a| a.anchor == reference)
1144 .map(|a| a.at)
1145 .ok_or_else(|| format!("'{reference}' is not a point of '{}' any more", source.id))
1146}
1147
1148fn lift(frame: &ViewFrame, placed: &PlacedView, p: [f64; 2]) -> [f64; 3] {
1151 let x = (p[0] - placed.position[0]) / placed.scale;
1152 let y = -(p[1] - placed.position[1]) / placed.scale;
1153 [
1154 frame.target[0] + frame.right[0] * x + frame.up[0] * y,
1155 frame.target[1] + frame.right[1] * x + frame.up[1] * y,
1156 frame.target[2] + frame.right[2] * x + frame.up[2] * y,
1157 ]
1158}
1159
1160fn detail_setup(
1182 sheet: &super::Sheet,
1183 drawn: &[ViewDrawing],
1184 circle: &super::DetailCircle,
1185 views: &[PmiView],
1186) -> Result<(DetailSetup, usize, DetailMark), String> {
1187 if circle.centre.is_empty() || circle.rim.is_empty() {
1190 return Err("no detail circle yet \u{2014} pick its centre and a point on its rim on a placed view".into());
1191 }
1192 let Some(source_index) = sheet.views.iter().position(|v| v.id == circle.source) else {
1193 return Err(format!("no placement '{}' to draw a detail of on this sheet", circle.source));
1194 };
1195 let source_placed = &sheet.views[source_index];
1196 if source_placed.detail.is_some() {
1197 return Err(format!(
1198 "'{}' is itself a detail; a detail of a detail is not drawn",
1199 circle.source
1200 ));
1201 }
1202 if source_placed.section.is_some() {
1203 return Err(format!(
1204 "'{}' is a section; a detail of a section is not drawn",
1205 circle.source
1206 ));
1207 }
1208 let source = &drawn[source_index];
1209 if !source.error.is_empty() {
1210 return Err(format!("placement '{}' did not project", circle.source));
1211 }
1212 let (centre, rim) = (point_of(source, &circle.centre)?, point_of(source, &circle.rim)?);
1213 let radius_mm = ((rim[0] - centre[0]).powi(2) + (rim[1] - centre[1]).powi(2)).sqrt();
1214 if !(radius_mm > 1e-9) {
1215 return Err(format!(
1216 "'{}' and '{}' land on the same point of '{}'; a detail circle needs a radius",
1217 circle.centre, circle.rim, circle.source
1218 ));
1219 }
1220 let Some(camera) = views
1221 .iter()
1222 .find(|view| view.id == source_placed.view)
1223 .and_then(|view| view.camera.as_ref())
1224 else {
1225 return Err(format!("placement '{}' has no captured camera to draw a detail of", circle.source));
1226 };
1227 if source_placed.scale <= 0.0 {
1228 return Err(format!("placement '{}' has no scale", circle.source));
1229 }
1230 let source_frame = ViewFrame::of(camera);
1231 let setup = DetailSetup {
1232 frame: ViewFrame { target: lift(&source_frame, source_placed, centre), ..source_frame },
1233 radius: radius_mm / source_placed.scale,
1234 label: circle.label.clone(),
1235 };
1236 let cap = if source.text_mm > 1e-9 { source.text_mm } else { 12.0 * MM_PER_POINT };
1237 let d = std::f64::consts::FRAC_1_SQRT_2;
1241 let reach = radius_mm + cap * 1.5;
1242 let mark = DetailMark {
1243 id: String::new(),
1244 label: circle.label.clone(),
1245 centre,
1246 radius_mm,
1247 lines: vec![circle_polyline(centre, radius_mm)],
1248 texts: vec![SheetText::flat(
1249 circle.label.clone(),
1250 [centre[0] + reach * d, centre[1] - reach * d],
1251 cap,
1252 )],
1253 };
1254 Ok((setup, source_index, mark))
1255}
1256
1257const CIRCLE_SEGMENTS: usize = 96;
1261
1262fn circle_polyline(centre: [f64; 2], radius: f64) -> Vec<[f64; 2]> {
1264 (0..=CIRCLE_SEGMENTS)
1265 .map(|k| {
1266 let t = std::f64::consts::TAU * k as f64 / CIRCLE_SEGMENTS as f64;
1267 [centre[0] + radius * t.cos(), centre[1] + radius * t.sin()]
1268 })
1269 .collect()
1270}
1271
1272fn clip_to_circle(lines: Vec<Vec<[f64; 2]>>, centre: [f64; 2], radius: f64) -> Vec<Vec<[f64; 2]>> {
1281 let mut out: Vec<Vec<[f64; 2]>> = Vec::new();
1282 let flush = |current: &mut Vec<[f64; 2]>, out: &mut Vec<Vec<[f64; 2]>>| {
1283 if current.len() >= 2 {
1284 out.push(std::mem::take(current));
1285 } else {
1286 current.clear();
1287 }
1288 };
1289 for line in lines {
1290 let mut current: Vec<[f64; 2]> = Vec::new();
1291 for pair in line.windows(2) {
1292 let (p, q) = (pair[0], pair[1]);
1293 let d = [q[0] - p[0], q[1] - p[1]];
1294 let f = [p[0] - centre[0], p[1] - centre[1]];
1295 let a = d[0] * d[0] + d[1] * d[1];
1296 let c = f[0] * f[0] + f[1] * f[1] - radius * radius;
1297 let span = if a < 1e-24 {
1299 (c <= 0.0).then_some((0.0, 1.0))
1300 } else {
1301 let b = 2.0 * (f[0] * d[0] + f[1] * d[1]);
1302 let disc = b * b - 4.0 * a * c;
1303 if disc < 0.0 {
1304 None
1305 } else {
1306 let root = disc.sqrt();
1307 let (t0, t1) = (((-b - root) / (2.0 * a)).max(0.0), ((-b + root) / (2.0 * a)).min(1.0));
1308 (t1 > t0).then_some((t0, t1))
1309 }
1310 };
1311 let at = |t: f64| [p[0] + d[0] * t, p[1] + d[1] * t];
1312 match span {
1313 None => flush(&mut current, &mut out),
1314 Some((t0, t1)) => {
1315 if t0 <= 0.0 {
1316 if current.is_empty() {
1317 current.push(p);
1318 }
1319 if t1 >= 1.0 {
1320 current.push(q);
1321 } else {
1322 current.push(at(t1));
1323 flush(&mut current, &mut out);
1324 }
1325 } else {
1326 flush(&mut current, &mut out);
1327 current.push(at(t0));
1328 if t1 >= 1.0 {
1329 current.push(q);
1330 } else {
1331 current.push(at(t1));
1332 flush(&mut current, &mut out);
1333 }
1334 }
1335 }
1336 }
1337 }
1338 flush(&mut current, &mut out);
1339 }
1340 out
1341}
1342
1343fn bounds_of(view: &ViewDrawing, placed: &PlacedView) -> [f64; 4] {
1344 let mut bounds = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
1345 let mut add = |p: [f64; 2]| {
1346 bounds[0] = bounds[0].min(p[0]);
1347 bounds[1] = bounds[1].min(p[1]);
1348 bounds[2] = bounds[2].max(p[0]);
1349 bounds[3] = bounds[3].max(p[1]);
1350 };
1351 for polyline in &view.edges {
1352 for point in polyline {
1353 add(*point);
1354 }
1355 }
1356 for annotation in &view.annotations {
1357 for polyline in &annotation.lines {
1358 for point in polyline {
1359 add(*point);
1360 }
1361 }
1362 for text in &annotation.texts {
1363 add(text.anchor);
1364 }
1365 }
1366 if let Some(section) = &view.section {
1367 for polyline in section.hatch.iter().chain(section.cut.iter()) {
1368 for point in polyline {
1369 add(*point);
1370 }
1371 }
1372 for text in §ion.texts {
1373 add(text.anchor);
1374 }
1375 }
1376 for mark in &view.section_marks {
1377 for polyline in &mark.lines {
1378 for point in polyline {
1379 add(*point);
1380 }
1381 }
1382 for text in &mark.texts {
1383 add(text.anchor);
1384 }
1385 }
1386 if let Some(detail) = &view.detail {
1387 for point in &detail.circle {
1388 add(*point);
1389 }
1390 for text in &detail.texts {
1391 add(text.anchor);
1392 }
1393 }
1394 for mark in &view.detail_marks {
1395 for polyline in &mark.lines {
1396 for point in polyline {
1397 add(*point);
1398 }
1399 }
1400 for text in &mark.texts {
1401 add(text.anchor);
1402 }
1403 }
1404 if bounds[0] > bounds[2] {
1405 [placed.position[0], placed.position[1], placed.position[0], placed.position[1]]
1406 } else {
1407 bounds
1408 }
1409}
1410
1411#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
1418pub struct SolidPose {
1419 pub translate: [f64; 3],
1420 #[serde(rename = "rotateDeg")]
1421 pub rotate_deg: [f64; 3],
1422 pub scale: [f64; 3],
1423 pub center: [f64; 3],
1424}
1425
1426impl SolidPose {
1427 pub fn apply(&self, p: [f64; 3]) -> [f64; 3] {
1428 let (center, scale, translate) = (self.center, self.scale, self.translate);
1429 let local = [(p[0] - center[0]) * scale[0], (p[1] - center[1]) * scale[1], (p[2] - center[2]) * scale[2]];
1430 let rotated = crate::engine_state::rotate_euler_xyz_f64(local, self.rotate_deg.map(f64::to_radians));
1432 [
1433 rotated[0] + center[0] + translate[0],
1434 rotated[1] + center[1] + translate[1],
1435 rotated[2] + center[2] + translate[2],
1436 ]
1437 }
1438
1439 pub fn matrix(&self) -> [f64; 16] {
1442 let radians = self.rotate_deg.map(f64::to_radians);
1443 let column = |axis: [f64; 3]| crate::engine_state::rotate_euler_xyz_f64(axis, radians);
1444 let cols = [
1445 column([self.scale[0], 0.0, 0.0]),
1446 column([0.0, self.scale[1], 0.0]),
1447 column([0.0, 0.0, self.scale[2]]),
1448 ];
1449 let c = self.center;
1450 let lc = [
1451 cols[0][0] * c[0] + cols[1][0] * c[1] + cols[2][0] * c[2],
1452 cols[0][1] * c[0] + cols[1][1] * c[1] + cols[2][1] * c[2],
1453 cols[0][2] * c[0] + cols[1][2] * c[1] + cols[2][2] * c[2],
1454 ];
1455 let t = [c[0] + self.translate[0] - lc[0], c[1] + self.translate[1] - lc[1], c[2] + self.translate[2] - lc[2]];
1456 [
1457 cols[0][0], cols[0][1], cols[0][2], 0.0,
1458 cols[1][0], cols[1][1], cols[1][2], 0.0,
1459 cols[2][0], cols[2][1], cols[2][2], 0.0,
1460 t[0], t[1], t[2], 1.0,
1461 ]
1462 }
1463
1464 fn identity(&self) -> String {
1466 let bits = |v: &[f64; 3]| format!("{:x},{:x},{:x}", v[0].to_bits(), v[1].to_bits(), v[2].to_bits());
1467 format!(
1468 "pose[{}|{}|{}|{}];",
1469 bits(&self.translate),
1470 bits(&self.rotate_deg),
1471 bits(&self.scale),
1472 bits(&self.center)
1473 )
1474 }
1475}
1476
1477pub(crate) fn poses_of(report: Option<&PmiViewReport>, name: &str) -> Vec<SolidPose> {
1481 let Some(report) = report else { return Vec::new() };
1482 report
1483 .annotations
1484 .iter()
1485 .filter(|row| row.enabled && row.status == PmiStatus::Ok)
1486 .filter_map(|row| match &row.geometry {
1487 brep_kernel::PmiGeometry::Explode { solids, translate, rotate_deg, scale, center, .. }
1488 if solids.iter().any(|target| target == name) =>
1489 {
1490 Some(SolidPose { translate: *translate, rotate_deg: *rotate_deg, scale: *scale, center: *center })
1491 }
1492 _ => None,
1493 })
1494 .collect()
1495}
1496
1497#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1501pub struct LinesSolid {
1502 pub name: String,
1503 pub handle: u32,
1504 pub fingerprint: u64,
1505 pub poses: Vec<SolidPose>,
1506 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1509 pub whole: bool,
1510}
1511
1512impl LinesSolid {
1513 fn identity(&self) -> String {
1518 let mut identity = format!("{}#{:016x};", self.name, self.fingerprint);
1519 for pose in &self.poses {
1520 identity.push_str(&pose.identity());
1521 }
1522 if self.whole {
1523 identity.push_str("whole;");
1524 }
1525 identity
1526 }
1527}
1528
1529struct ExactPlan<'a> {
1533 solids: Vec<LinesSolid>,
1534 sources: Vec<&'a brep_kernel::BrepSolid>,
1536 each: Vec<String>,
1538 identity: String,
1539}
1540
1541impl ExactPlan<'_> {
1542 fn posed(&self) -> Vec<std::borrow::Cow<'_, brep_kernel::BrepSolid>> {
1549 self.solids
1550 .iter()
1551 .zip(&self.sources)
1552 .map(|(solid, source)| posed_solid(std::borrow::Cow::Borrowed(*source), &solid.poses))
1553 .collect()
1554 }
1555}
1556
1557fn posed_solid<'a>(
1558 solid: std::borrow::Cow<'a, brep_kernel::BrepSolid>,
1559 poses: &[SolidPose],
1560) -> std::borrow::Cow<'a, brep_kernel::BrepSolid> {
1561 poses.iter().fold(solid, |solid, pose| std::borrow::Cow::Owned(pose_solid(&solid, &|p| pose.apply(p))))
1562}
1563
1564fn left_whole(view: &PmiView, name: &str) -> bool {
1566 view.display.section.as_ref().is_some_and(|section| section.excludes(name))
1567}
1568
1569fn exact_solids<'a>(scene: &'a RenderScene, unposed: &Unposed, view: &PmiView, report: Option<&PmiViewReport>) -> Option<ExactPlan<'a>> {
1572 let mut plan = ExactPlan { solids: Vec::new(), sources: Vec::new(), each: Vec::new(), identity: String::new() };
1573 for solid in drawn_solids(scene, unposed, view) {
1574 let (brep, fingerprint) = scene.exact_solid_fingerprinted(&solid.name)?;
1575 let planned = LinesSolid {
1576 name: solid.name.clone(),
1577 handle: solid.source_handle,
1578 fingerprint,
1579 poses: poses_of(report, &solid.name),
1580 whole: left_whole(view, &solid.name),
1581 };
1582 plan.each.push(planned.identity());
1583 plan.solids.push(planned);
1584 plan.sources.push(brep);
1585 }
1586 plan.identity = plan.each.concat();
1587 Some(plan)
1588}
1589
1590#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
1598pub enum LinesClip {
1599 Whole,
1600 Circle { radius: f64 },
1601 Section { half: Option<super::hlr::HalfLine> },
1602}
1603
1604impl LinesClip {
1605 fn key(&self) -> String {
1607 match self {
1608 LinesClip::Whole => "whole".into(),
1609 LinesClip::Circle { radius } => format!("circle {:x}", radius.to_bits()),
1610 LinesClip::Section { half: None } => "section".into(),
1611 LinesClip::Section { half: Some(half) } => format!(
1612 "section half {:x},{:x},{:x}",
1613 half.normal[0].to_bits(),
1614 half.normal[1].to_bits(),
1615 half.offset.to_bits()
1616 ),
1617 }
1618 }
1619}
1620
1621#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1625pub struct LinesJob {
1626 pub key: String,
1628 pub placement: String,
1631 pub solids: Vec<LinesSolid>,
1632 pub frame: ViewFrame,
1633 pub placed: PlacedView,
1635 pub clip: LinesClip,
1636}
1637
1638pub enum LinesMode<'a> {
1640 Compute,
1643 Defer(&'a mut dyn FnMut(LinesJob) -> Deferred),
1648 Export(&'a mut dyn FnMut(LinesJob) -> Deferred),
1650}
1651
1652pub enum Deferred {
1654 Lines(ExactLines),
1655 Pending,
1656 Here,
1657}
1658
1659pub fn run_lines_job(job: &LinesJob) -> Result<ExactLines, String> {
1665 let mut posed = Vec::with_capacity(job.solids.len());
1666 for solid in &job.solids {
1667 let brep = brep_kernel::registered_solid_clone(solid.handle)
1668 .map_err(|error| format!("'{}' (handle {}): {error}", solid.name, solid.handle))?;
1669 if crate::scene::brep_fingerprint(&brep) != solid.fingerprint {
1670 return Err(format!("'{}' changed under handle {}", solid.name, solid.handle));
1671 }
1672 posed.push(posed_solid(std::borrow::Cow::Owned(brep), &solid.poses).into_owned());
1673 }
1674 let solids: Vec<&brep_kernel::BrepSolid> = posed.iter().collect();
1675 let each: Vec<String> = job.solids.iter().map(LinesSolid::identity).collect();
1676 let whole: Vec<bool> = job.solids.iter().map(|solid| solid.whole).collect();
1677 Ok(compute_exact_lines(&solids, &each, &whole, &job.frame, &job.placed, job.clip))
1678}
1679
1680fn compute_exact_lines(
1683 solids: &[&brep_kernel::BrepSolid],
1684 each: &[String],
1685 whole: &[bool],
1686 frame: &ViewFrame,
1687 origin: &PlacedView,
1688 clip: LinesClip,
1689) -> ExactLines {
1690 let trims = solid_trims(each, solids);
1691 let trims: Vec<&super::hlr::SolidTrims> = trims.iter().map(|t| t.as_ref()).collect();
1692 match clip {
1693 LinesClip::Whole => ExactLines {
1694 runs: super::hlr::visible_edges_with(solids, &trims, frame, origin, &super::hlr::Clip::Whole).runs,
1695 cut: Vec::new(),
1696 },
1697 LinesClip::Circle { radius } => ExactLines {
1698 runs: super::hlr::visible_edges_with(solids, &trims, frame, origin, &super::hlr::Clip::Circle { radius }).runs,
1699 cut: Vec::new(),
1700 },
1701 LinesClip::Section { half } => {
1702 let cut_by = |index: &usize| !whole.get(*index).copied().unwrap_or(false);
1708 let cut_solids: Vec<&brep_kernel::BrepSolid> = (0..solids.len()).filter(cut_by).map(|i| solids[i]).collect();
1709 let cut_trims: Vec<&super::hlr::SolidTrims> = (0..solids.len()).filter(cut_by).map(|i| trims[i]).collect();
1710 let cut = super::hlr::section_curves_with(&cut_solids, &cut_trims, frame, origin);
1711 let cut = match half {
1712 Some(half) => close_on_half(cut, half, origin),
1713 None => cut,
1714 };
1715 let clip = super::hlr::Clip::Beyond { cap: &cut, whole, half };
1716 ExactLines { runs: super::hlr::visible_edges_with(solids, &trims, frame, origin, &clip).runs, cut }
1717 }
1718 }
1719}
1720
1721fn exact_lines_for(
1725 plan: &ExactPlan,
1726 frame: &ViewFrame,
1727 placed: &PlacedView,
1728 clip: LinesClip,
1729 mode: &mut LinesMode,
1730) -> Option<ExactLines> {
1731 let key = exact_key(&plan.identity, frame, placed.scale, &clip.key());
1732 let origin = PlacedView { position: [0.0, 0.0], ..placed.clone() };
1733 let relative = match cached_exact_lines(&key) {
1734 Some(lines) => lines,
1735 None => {
1736 let deferred = match mode {
1737 LinesMode::Compute => Deferred::Here,
1738 LinesMode::Defer(defer) | LinesMode::Export(defer) => defer(LinesJob {
1739 key: key.clone(),
1740 placement: placed.id.clone(),
1741 solids: plan.solids.clone(),
1742 frame: *frame,
1743 placed: origin.clone(),
1744 clip,
1745 }),
1746 };
1747 let lines = match deferred {
1748 Deferred::Lines(lines) => lines,
1749 Deferred::Pending => return None,
1750 Deferred::Here => {
1751 let posed = plan.posed();
1752 let solids: Vec<&brep_kernel::BrepSolid> = posed.iter().map(|solid| solid.as_ref()).collect();
1753 let whole: Vec<bool> = plan.solids.iter().map(|solid| solid.whole).collect();
1754 compute_exact_lines(&solids, &plan.each, &whole, frame, &origin, clip)
1755 }
1756 };
1757 store_exact_lines(key, lines.clone());
1758 lines
1759 }
1760 };
1761 let shift = |lines: Vec<Vec<[f64; 2]>>| -> Vec<Vec<[f64; 2]>> {
1762 lines
1763 .into_iter()
1764 .map(|line| line.into_iter().map(|p| [p[0] + placed.position[0], p[1] + placed.position[1]]).collect())
1765 .collect()
1766 };
1767 Some(ExactLines { runs: shift(relative.runs), cut: shift(relative.cut) })
1768}
1769
1770const TRIM_CACHE_SOLIDS: usize = EXACT_CACHE_ENTRIES;
1773
1774thread_local! {
1775 static TRIM_CACHE: std::cell::RefCell<std::collections::VecDeque<(String, std::rc::Rc<super::hlr::SolidTrims>)>> =
1780 const { std::cell::RefCell::new(std::collections::VecDeque::new()) };
1781}
1782
1783
1784
1785fn solid_trims(identities: &[String], solids: &[&brep_kernel::BrepSolid]) -> Vec<std::rc::Rc<super::hlr::SolidTrims>> {
1787 TRIM_CACHE.with(|cache| {
1788 let mut cache = cache.borrow_mut();
1789 identities
1790 .iter()
1791 .zip(solids)
1792 .map(|(identity, solid)| {
1793 let trims = match cache.iter().position(|(key, _)| key == identity) {
1794 Some(index) => cache.remove(index).map(|(_, trims)| trims).unwrap(),
1795 None => std::rc::Rc::new(super::hlr::SolidTrims::of(solid)),
1796 };
1797 cache.push_back((identity.clone(), trims.clone()));
1798 while cache.len() > TRIM_CACHE_SOLIDS {
1799 cache.pop_front();
1800 }
1801 trims
1802 })
1803 .collect()
1804 })
1805}
1806
1807#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
1811pub struct ExactLines {
1812 pub runs: Vec<Vec<[f64; 2]>>,
1813 pub cut: Vec<Vec<[f64; 2]>>,
1814}
1815
1816const EXACT_CACHE_ENTRIES: usize = 64;
1818
1819thread_local! {
1820 static EXACT_CACHE: std::cell::RefCell<std::collections::VecDeque<(String, ExactLines)>> =
1827 const { std::cell::RefCell::new(std::collections::VecDeque::new()) };
1828}
1829
1830pub(crate) fn cached_exact_lines(key: &str) -> Option<ExactLines> {
1832 EXACT_CACHE.with(|cache| cache.borrow().iter().find(|(k, _)| k == key).map(|(_, lines)| lines.clone()))
1833}
1834
1835pub(crate) fn store_exact_lines(key: String, lines: ExactLines) {
1838 EXACT_CACHE.with(|cache| {
1839 let mut cache = cache.borrow_mut();
1840 cache.retain(|(k, _)| *k != key);
1841 cache.push_back((key, lines));
1842 while cache.len() > EXACT_CACHE_ENTRIES {
1843 cache.pop_front();
1844 }
1845 });
1846}
1847
1848fn exact_key(identity: &str, frame: &ViewFrame, scale: f64, clip: &str) -> String {
1850 let bits = |v: [f64; 3]| format!("{:x},{:x},{:x}", v[0].to_bits(), v[1].to_bits(), v[2].to_bits());
1851 format!(
1852 "{identity}|{}|{}|{}|{}|{:x}|{clip}",
1853 bits(frame.target),
1854 bits(frame.right),
1855 bits(frame.up),
1856 bits(frame.forward),
1857 scale.to_bits()
1858 )
1859}
1860
1861fn pose_solid(solid: &brep_kernel::BrepSolid, pose: &dyn Fn([f64; 3]) -> [f64; 3]) -> brep_kernel::BrepSolid {
1868 let map = |cp: &brep_kernel::Vec4| -> brep_kernel::Vec4 {
1869 let w = if cp.w != 0.0 { cp.w } else { 1.0 };
1870 let p = pose([cp.x / w, cp.y / w, cp.z / w]);
1871 brep_kernel::Vec4 { x: p[0] * w, y: p[1] * w, z: p[2] * w, w: cp.w }
1872 };
1873 let mut out = solid.clone();
1874 for vertex in &mut out.vertices {
1875 let p = pose([vertex.point.x, vertex.point.y, vertex.point.z]);
1876 vertex.point = brep_kernel::Vec3::new(p[0], p[1], p[2]);
1877 }
1878 for edge in &mut out.edges {
1879 let points = edge.curve.control_points.iter().map(map).collect();
1880 if let Ok(curve) = brep_kernel::NurbsCurve::new(edge.curve.degree, edge.curve.knots.clone(), points) {
1881 edge.curve = curve;
1882 }
1883 }
1884 for shell in &mut out.shells {
1885 for face in &mut shell.faces {
1886 let s = &face.surface;
1887 let points = s.control_points.iter().map(|row| row.iter().map(map).collect()).collect();
1888 if let Ok(surface) =
1889 brep_kernel::NurbsSurface::new(s.degree_u, s.degree_v, s.knots_u.clone(), s.knots_v.clone(), points)
1890 {
1891 face.surface = surface;
1892 }
1893 }
1894 }
1895 out
1896}
1897
1898fn close_on_half(cut: Vec<Vec<[f64; 2]>>, half: super::hlr::HalfLine, origin: &PlacedView) -> Vec<Vec<[f64; 2]>> {
1905 let side = |p: [f64; 2]| {
1906 half.at([(p[0] - origin.position[0]) / origin.scale, (origin.position[1] - p[1]) / origin.scale, 0.0])
1907 };
1908 let mut out: Vec<Vec<[f64; 2]>> = Vec::new();
1909 let mut crossings: Vec<[f64; 2]> = Vec::new();
1910 for line in cut {
1911 let mut run: Vec<[f64; 2]> = Vec::new();
1912 for (i, &p) in line.iter().enumerate() {
1913 let g = side(p);
1914 if i > 0 {
1915 let q = line[i - 1];
1916 let h = side(q);
1917 if (h < 0.0) != (g < 0.0) {
1918 let t = h / (h - g);
1919 let x = [q[0] + (p[0] - q[0]) * t, q[1] + (p[1] - q[1]) * t];
1920 crossings.push(x);
1921 run.push(x);
1922 if g >= 0.0 {
1923 out.push(std::mem::take(&mut run));
1924 }
1925 }
1926 }
1927 if g < 0.0 {
1928 run.push(p);
1929 }
1930 }
1931 if run.len() > 1 {
1932 out.push(run);
1933 }
1934 }
1935 let along = |p: &[f64; 2]| -p[0] * half.normal[1] - p[1] * half.normal[0];
1938 crossings.sort_by(|a, b| along(a).partial_cmp(&along(b)).unwrap_or(std::cmp::Ordering::Equal));
1939 out.extend(crossings.chunks_exact(2).map(|pair| vec![pair[0], pair[1]]));
1940 out.retain(|line| line.len() > 1);
1941 out
1942}
1943
1944fn segments_of(lines: &[Vec<[f64; 2]>]) -> Vec<Vec<[f64; 2]>> {
1945 lines.iter().flat_map(|line| line.windows(2).map(|w| vec![w[0], w[1]])).collect()
1946}
1947
1948fn drawn_solids<'a>(scene: &'a RenderScene, unposed: &'a Unposed, view: &PmiView) -> Vec<&'a crate::scene::SolidDisplay> {
1957 scene
1958 .solids()
1959 .iter()
1960 .map(|solid| unposed.get(&solid.name).unwrap_or(solid))
1961 .filter(|solid| solid.visible && !solid.is_sketch)
1962 .filter(|solid| !view.display.hidden.iter().any(|name| name == &solid.name))
1963 .filter(|solid| solid.mesh.indices.len() >= 3)
1964 .collect()
1965}
1966
1967pub fn sheet_solid_names(scene: &RenderScene, unposed: &Unposed, sheet: &super::Sheet, views: &[PmiView]) -> Vec<String> {
1970 let mut names: Vec<String> = Vec::new();
1971 for placed in &sheet.views {
1972 let Some(view) = views.iter().find(|view| view.id == placed.view) else { continue };
1973 for solid in drawn_solids(scene, unposed, view) {
1974 if !names.contains(&solid.name) {
1975 names.push(solid.name.clone());
1976 }
1977 }
1978 }
1979 names
1980}
1981
1982fn drawn_displays<'a>(scene: &'a RenderScene, unposed: &'a Unposed, view: &PmiView) -> Vec<&'a crate::scene::SolidDisplay> {
1986 drawn_solids(scene, unposed, view)
1987}
1988
1989
1990fn mesh_solids(displays: &[&crate::scene::SolidDisplay], report: Option<&PmiViewReport>) -> Vec<super::hlr::MeshSolid> {
2006 let world = |p: &[f32; 3]| [p[0] as f64, p[1] as f64, p[2] as f64];
2007 displays
2008 .iter()
2009 .map(|solid| {
2010 let poses = poses_of(report, &solid.name);
2011 let pose = |p: [f64; 3]| poses.iter().fold(p, |p, pose| pose.apply(p));
2012 let count = solid.mesh.positions.len();
2013 let (triangles, faces) = solid
2014 .mesh
2015 .indices
2016 .chunks_exact(3)
2017 .enumerate()
2018 .filter(|(_, c)| c.iter().all(|i| (*i as usize) < count))
2019 .map(|(t, c)| ([c[0], c[1], c[2]], solid.mesh.face_ids.get(t).copied().unwrap_or(u32::MAX)))
2020 .unzip();
2021 let bits = |p: [f32; 3]| [p[0].to_bits(), p[1].to_bits(), p[2].to_bits()];
2022 let mut at: std::collections::HashMap<[u32; 3], Option<u64>> = std::collections::HashMap::new();
2023 for vertex in &solid.vertices {
2024 let p = vertex.position;
2025 at.entry(bits([p[0] as f32, p[1] as f32, p[2] as f32]))
2026 .and_modify(|id| *id = None)
2027 .or_insert(Some(vertex.topo_id));
2028 }
2029 let vertex_of = |p: &[f32; 3]| at.get(&bits(*p)).copied().flatten();
2030 super::hlr::MeshSolid {
2031 positions: solid.mesh.positions.iter().map(|p| pose(world(p))).collect(),
2032 triangles,
2033 faces,
2034 edges: solid
2035 .edges
2036 .iter()
2037 .filter(|edge| !edge.aux && !edge.centerline && edge.polyline.len() >= 2)
2038 .map(|edge| super::hlr::MeshEdge {
2039 points: edge.polyline.iter().map(|p| pose(world(p))).collect(),
2040 ends: vertex_of(&edge.polyline[0]).zip(vertex_of(edge.polyline.last().unwrap())),
2041 })
2042 .collect(),
2043 }
2044 })
2045 .collect()
2046}
2047
2048const MESH_CACHE_ENTRIES: usize = 16;
2050
2051thread_local! {
2052 static MESH_CACHE: std::cell::RefCell<std::collections::VecDeque<(String, ExactLines)>> =
2059 const { std::cell::RefCell::new(std::collections::VecDeque::new()) };
2060}
2061
2062#[allow(clippy::too_many_arguments)]
2065fn mesh_lines_for(
2066 scene: &RenderScene,
2067 unposed: &Unposed,
2068 view: &PmiView,
2069 report: Option<&PmiViewReport>,
2070 frame: &ViewFrame,
2071 placed: &PlacedView,
2072 clip: LinesClip,
2073 plane: Option<&ClipPlane>,
2074) -> ExactLines {
2075 let displays = drawn_displays(scene, unposed, view);
2076 let mut identity = String::from("mesh|");
2077 let whole: Vec<bool> = displays.iter().map(|display| left_whole(view, &display.name)).collect();
2078 for (display, whole) in displays.iter().zip(&whole) {
2079 identity.push_str(&format!("{}@{};", display.name, display.revision));
2080 for pose in poses_of(report, &display.name) {
2081 identity.push_str(&pose.identity());
2082 }
2083 if *whole {
2084 identity.push_str("whole;");
2085 }
2086 }
2087 let bits = |v: [f64; 3]| format!("{:x},{:x},{:x}", v[0].to_bits(), v[1].to_bits(), v[2].to_bits());
2088 let plane_key = plane.map(|plane| format!("{}|{}", bits(plane.point), bits(plane.normal))).unwrap_or_default();
2089 let key = exact_key(&identity, frame, placed.scale, &format!("{}{plane_key}", clip.key()));
2090 let hit = MESH_CACHE.with(|cache| cache.borrow().iter().find(|(k, _)| *k == key).map(|(_, lines)| lines.clone()));
2091 let relative = match hit {
2092 Some(lines) => lines,
2093 None => {
2094 let origin = PlacedView { position: [0.0, 0.0], ..placed.clone() };
2095 let meshes = mesh_solids(&displays, report);
2096 let lines = match (clip, plane) {
2097 (LinesClip::Section { half }, Some(plane)) => {
2098 let cut_meshes: Vec<_> = meshes.iter().zip(&whole).filter(|(_, whole)| !**whole).map(|(mesh, _)| mesh.clone()).collect();
2099 let cut: Vec<Vec<[f64; 2]>> = mesh_cut(&cut_meshes, plane)
2100 .iter()
2101 .map(|segment| {
2102 segment
2103 .iter()
2104 .map(|p| {
2105 let v = frame.to_view(*p);
2106 to_paper(&origin, v[0], v[1])
2107 })
2108 .collect()
2109 })
2110 .collect();
2111 let cut = match half {
2112 Some(half) => close_on_half(cut, half, &origin),
2113 None => cut,
2114 };
2115 let beyond = super::hlr::Clip::Beyond { cap: &cut, whole: &whole, half };
2116 ExactLines { runs: super::hlr::visible_mesh_lines(&meshes, frame, &origin, &beyond).runs, cut }
2117 }
2118 (LinesClip::Circle { radius }, _) => ExactLines {
2119 runs: super::hlr::visible_mesh_lines(&meshes, frame, &origin, &super::hlr::Clip::Circle { radius }).runs,
2120 cut: Vec::new(),
2121 },
2122 _ => ExactLines {
2123 runs: super::hlr::visible_mesh_lines(&meshes, frame, &origin, &super::hlr::Clip::Whole).runs,
2124 cut: Vec::new(),
2125 },
2126 };
2127 MESH_CACHE.with(|cache| {
2128 let mut cache = cache.borrow_mut();
2129 cache.push_back((key, lines.clone()));
2130 while cache.len() > MESH_CACHE_ENTRIES {
2131 cache.pop_front();
2132 }
2133 });
2134 lines
2135 }
2136 };
2137 let shift = |lines: Vec<Vec<[f64; 2]>>| -> Vec<Vec<[f64; 2]>> {
2138 lines
2139 .into_iter()
2140 .map(|line| line.into_iter().map(|p| [p[0] + placed.position[0], p[1] + placed.position[1]]).collect())
2141 .collect()
2142 };
2143 ExactLines { runs: shift(relative.runs), cut: shift(relative.cut) }
2144}
2145
2146#[derive(Debug, Clone, Copy)]
2161pub struct ClipPlane {
2162 pub point: [f64; 3],
2163 pub normal: [f64; 3],
2164}
2165
2166fn mesh_cut(meshes: &[super::hlr::MeshSolid], plane: &ClipPlane) -> Vec<[[f64; 3]; 2]> {
2171 let mut extent: f64 = 0.0;
2175 for mesh in meshes {
2176 for p in &mesh.positions {
2177 extent = extent.max(dot(sub(*p, plane.point), plane.normal).abs());
2178 }
2179 }
2180 let eps = (extent * 1e-9).max(f64::MIN_POSITIVE);
2181
2182 let mut cut: Vec<[[f64; 3]; 2]> = Vec::new();
2183 for mesh in meshes {
2184 let side: Vec<f64> = mesh
2185 .positions
2186 .iter()
2187 .map(|p| dot(sub(*p, plane.point), plane.normal))
2188 .collect();
2189 for tri in &mesh.triangles {
2190 let corners = [tri[0] as usize, tri[1] as usize, tri[2] as usize];
2191 let d = [side[corners[0]], side[corners[1]], side[corners[2]]];
2192 if d.iter().all(|v| *v < -eps) || d.iter().all(|v| *v > eps) {
2193 continue; }
2195 let mut kept = 0;
2198 let mut on_plane: Vec<[f64; 3]> = Vec::with_capacity(2);
2199 for k in 0..3 {
2200 let (i, j) = (k, (k + 1) % 3);
2201 let (pi, pj) = (mesh.positions[corners[i]], mesh.positions[corners[j]]);
2202 let (di, dj) = (d[i], d[j]);
2203 if di >= -eps {
2204 kept += 1;
2205 if di.abs() <= eps {
2206 on_plane.push(pi);
2207 }
2208 }
2209 if (di > eps && dj < -eps) || (di < -eps && dj > eps) {
2210 let t = di / (di - dj);
2211 kept += 1;
2212 on_plane.push([
2213 pi[0] + (pj[0] - pi[0]) * t,
2214 pi[1] + (pj[1] - pi[1]) * t,
2215 pi[2] + (pj[2] - pi[2]) * t,
2216 ]);
2217 }
2218 }
2219 if kept >= 3 && on_plane.len() == 2 {
2233 cut.push([on_plane[0], on_plane[1]]);
2234 }
2235 }
2236 }
2237 cut
2238}
2239
2240fn hatch(cut: &[Vec<[f64; 2]>]) -> Vec<Vec<[f64; 2]>> {
2250 if cut.is_empty() {
2251 return Vec::new();
2252 }
2253 let d = std::f64::consts::FRAC_1_SQRT_2;
2256 let u = [d, -d];
2257 let n = [d, d];
2258 let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
2259 for segment in cut {
2260 for p in segment {
2261 let c = p[0] * n[0] + p[1] * n[1];
2262 lo = lo.min(c);
2263 hi = hi.max(c);
2264 }
2265 }
2266 if !lo.is_finite() || hi <= lo {
2267 return Vec::new();
2268 }
2269 let mut spacing = HATCH_SPACING_MM;
2270 while (hi - lo) / spacing > MAX_HATCH_LINES as f64 {
2271 spacing *= 2.0;
2272 }
2273 let first = (lo / spacing).ceil() * spacing;
2276 let mut out = Vec::new();
2277 let mut c = first;
2278 while c <= hi {
2279 let mut crossings: Vec<f64> = Vec::new();
2280 for segment in cut {
2281 let (a, b) = (segment[0], segment[segment.len() - 1]);
2282 let (ca, cb) = (a[0] * n[0] + a[1] * n[1], b[0] * n[0] + b[1] * n[1]);
2283 if (ca <= c) == (cb <= c) {
2286 continue;
2287 }
2288 let t = (c - ca) / (cb - ca);
2289 let hit = [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
2290 crossings.push(hit[0] * u[0] + hit[1] * u[1]);
2291 }
2292 crossings.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
2293 for pair in crossings.chunks_exact(2) {
2294 if pair[1] - pair[0] < 1e-9 {
2295 continue;
2296 }
2297 out.push(vec![
2298 [u[0] * pair[0] + n[0] * c, u[1] * pair[0] + n[1] * c],
2299 [u[0] * pair[1] + n[0] * c, u[1] * pair[1] + n[1] * c],
2300 ]);
2301 }
2302 c += spacing;
2303 }
2304 out
2305}
2306
2307const CIRCLE_MIN_SPAN_DEG: f64 = 20.0;
2316
2317const CIRCLE_FIT_TOLERANCE: f64 = 1e-4;
2322
2323const STRAIGHT_TOLERANCE: f64 = 1e-4;
2328
2329fn anchor_points(
2337 scene: &RenderScene,
2338 unposed: &Unposed,
2339 view: &PmiView,
2340 report: Option<&PmiViewReport>,
2341 frame: &ViewFrame,
2342 placed: &PlacedView,
2343) -> Vec<AnchorPoint> {
2344 let project = |p: [f64; 3]| -> [f64; 2] {
2345 let v = frame.to_view(p);
2346 to_paper(placed, v[0], v[1])
2347 };
2348 let mut out = Vec::new();
2349 for solid in drawn_displays(scene, unposed, view) {
2350 let poses = poses_of(report, &solid.name);
2353 let pose = |p: [f64; 3]| poses.iter().fold(p, |p, pose| pose.apply(p));
2354 for vertex in &solid.vertices {
2355 out.push(AnchorPoint {
2356 anchor: super::dimension::SheetAnchor::vertex(&placed.id, &solid.name, vertex.topo_id)
2357 .to_ref(),
2358 kind: super::dimension::VERTEX.to_string(),
2359 at: project(pose(vertex.position)),
2360 model: pose(vertex.position),
2361 dir: None,
2362 radius: None,
2363 u: None,
2364 v: None,
2365 });
2366 }
2367 for edge in &solid.edges {
2368 if edge.name.is_empty() || edge.aux || edge.centerline || edge.polyline.len() < 2 {
2372 continue;
2373 }
2374 let points: Vec<[f64; 3]> = edge
2375 .polyline
2376 .iter()
2377 .map(|p| pose([p[0] as f64, p[1] as f64, p[2] as f64]))
2378 .collect();
2379 let dir = straight_chord(&points).and_then(|d| {
2384 let paper = [dot(d, frame.right), -dot(d, frame.up)];
2385 let length = (paper[0] * paper[0] + paper[1] * paper[1]).sqrt();
2386 (length > 1e-9).then(|| [paper[0] / length, paper[1] / length])
2387 });
2388 for fraction in [0.0, 0.5, 1.0] {
2389 let Some(point) = along(&points, fraction) else { continue };
2390 out.push(AnchorPoint {
2391 anchor: super::dimension::SheetAnchor::edge(
2392 &placed.id,
2393 &solid.name,
2394 &edge.name,
2395 fraction,
2396 )
2397 .to_ref(),
2398 kind: super::dimension::EDGE.to_string(),
2399 at: project(point),
2400 model: point,
2401 dir,
2402 radius: None,
2403 u: None,
2404 v: None,
2405 });
2406 }
2407 if let Some((centre, axis, radius)) = fit_circle(&points) {
2408 let u3 = norm(sub(points[0], centre));
2412 let v3 = cross(axis, u3);
2413 let image = |d: [f64; 3]| -> [f64; 2] {
2414 [
2415 dot(d, frame.right) * radius * placed.scale,
2416 -dot(d, frame.up) * radius * placed.scale,
2417 ]
2418 };
2419 out.push(AnchorPoint {
2420 anchor: super::dimension::SheetAnchor::circle(&placed.id, &solid.name, &edge.name)
2421 .to_ref(),
2422 kind: super::dimension::CIRCLE.to_string(),
2423 at: project(centre),
2424 model: centre,
2425 dir: None,
2426 radius: Some(radius),
2427 u: Some(image(u3)),
2428 v: Some(image(v3)),
2429 });
2430 }
2431 }
2432 }
2433 out
2434}
2435
2436fn straight_chord(points: &[[f64; 3]]) -> Option<[f64; 3]> {
2444 let (first, last) = (*points.first()?, *points.last()?);
2445 let span = sub(last, first);
2446 let length = dot(span, span).sqrt();
2447 if length < 1e-12 {
2448 return None;
2450 }
2451 let direction = [span[0] / length, span[1] / length, span[2] / length];
2452 let bar = length * STRAIGHT_TOLERANCE;
2453 for point in points {
2454 let offset = sub(*point, first);
2455 let along = dot(offset, direction);
2456 let away = [
2457 offset[0] - direction[0] * along,
2458 offset[1] - direction[1] * along,
2459 offset[2] - direction[2] * along,
2460 ];
2461 if dot(away, away).sqrt() > bar {
2462 return None;
2463 }
2464 }
2465 Some(direction)
2466}
2467
2468fn along(points: &[[f64; 3]], fraction: f64) -> Option<[f64; 3]> {
2470 if points.len() < 2 {
2471 return None;
2472 }
2473 let steps: Vec<f64> = points
2474 .windows(2)
2475 .map(|pair| dot(sub(pair[1], pair[0]), sub(pair[1], pair[0])).sqrt())
2476 .collect();
2477 let total: f64 = steps.iter().sum();
2478 if !(total > 0.0) {
2479 return Some(points[0]);
2480 }
2481 let target = total * fraction.clamp(0.0, 1.0);
2482 let mut walked = 0.0;
2483 for (index, step) in steps.iter().enumerate() {
2484 if walked + step >= target || index + 1 == steps.len() {
2485 let t = if *step > 0.0 { ((target - walked) / step).clamp(0.0, 1.0) } else { 0.0 };
2486 let (a, b) = (points[index], points[index + 1]);
2487 return Some([
2488 a[0] + (b[0] - a[0]) * t,
2489 a[1] + (b[1] - a[1]) * t,
2490 a[2] + (b[2] - a[2]) * t,
2491 ]);
2492 }
2493 walked += step;
2494 }
2495 points.last().copied()
2496}
2497
2498fn fit_circle(points: &[[f64; 3]]) -> Option<([f64; 3], [f64; 3], f64)> {
2507 if points.len() < 4 {
2508 return None;
2509 }
2510 let last = points.len() - 1;
2511 let (a, b, c) = (points[0], points[last / 3], points[last * 2 / 3]);
2512 let (ab, ac) = (sub(b, a), sub(c, a));
2513 let n = cross(ab, ac);
2514 let n2 = dot(n, n);
2515 if n2 < 1e-24 {
2516 return None;
2517 }
2518 let centre = {
2520 let (ab2, ac2) = (dot(ab, ab), dot(ac, ac));
2521 let to = cross(
2522 [
2523 ab2 * ac[0] - ac2 * ab[0],
2524 ab2 * ac[1] - ac2 * ab[1],
2525 ab2 * ac[2] - ac2 * ab[2],
2526 ],
2527 n,
2528 );
2529 [
2530 a[0] + to[0] / (2.0 * n2),
2531 a[1] + to[1] / (2.0 * n2),
2532 a[2] + to[2] / (2.0 * n2),
2533 ]
2534 };
2535 let axis = norm(n);
2536 let radius = dot(sub(a, centre), sub(a, centre)).sqrt();
2537 if !(radius > 1e-12) {
2538 return None;
2539 }
2540 let bar = radius * CIRCLE_FIT_TOLERANCE;
2541 for p in points {
2542 let d = sub(*p, centre);
2543 if (dot(d, d).sqrt() - radius).abs() > bar || dot(d, axis).abs() > bar {
2544 return None;
2545 }
2546 }
2547 let first = norm(sub(points[0], centre));
2550 let mut span = 0.0;
2551 for p in points {
2552 let d = norm(sub(*p, centre));
2553 let angle = dot(d, first).clamp(-1.0, 1.0).acos();
2554 span = f64::max(span, angle);
2555 }
2556 if span.to_degrees() < CIRCLE_MIN_SPAN_DEG {
2557 return None;
2558 }
2559 Some((centre, axis, radius))
2560}
2561
2562#[derive(Debug, Clone, Copy)]
2571struct Box2 {
2572 centre: [f64; 2],
2573 right: [f64; 2],
2574 up: [f64; 2],
2575 half: [f64; 2],
2576}
2577
2578impl Box2 {
2579 fn local(&self, p: [f64; 2]) -> Option<[f64; 2]> {
2583 let det = self.right[0] * self.up[1] - self.right[1] * self.up[0];
2584 if det.abs() < 1e-12 {
2585 return None;
2586 }
2587 let d = [p[0] - self.centre[0], p[1] - self.centre[1]];
2588 Some([
2589 (d[0] * self.up[1] - d[1] * self.up[0]) / det,
2590 (d[1] * self.right[0] - d[0] * self.right[1]) / det,
2591 ])
2592 }
2593
2594 fn of_text(run: &SheetText) -> Box2 {
2597 let rows = run.text.lines().count().max(1) as f64;
2598 Box2 {
2599 centre: run.anchor,
2600 right: run.right,
2601 up: run.up,
2602 half: [text_half_width(&run.text, 1.0), rows * 0.5 + TEXT_PAD],
2603 }
2604 }
2605}
2606
2607pub(super) fn text_half_width(text: &str, height: f64) -> f64 {
2612 let columns = text.lines().map(|line| line.chars().count()).max().unwrap_or(0) as f64;
2613 (columns * TEXT_ADVANCE * 0.5 + TEXT_PAD) * height
2614}
2615
2616fn break_for_text(lines: Vec<Vec<[f64; 2]>>, boxes: &[Box2]) -> Vec<Vec<[f64; 2]>> {
2629 if boxes.is_empty() {
2630 return lines;
2631 }
2632 let mut out: Vec<Vec<[f64; 2]>> = Vec::new();
2633 for line in lines {
2634 let mut current: Vec<[f64; 2]> = Vec::new();
2636 for pair in line.windows(2) {
2637 let (p, q) = (pair[0], pair[1]);
2638 let mut kept = visible_spans(p, q, boxes);
2639 if kept.is_empty() {
2640 if current.len() >= 2 {
2641 out.push(std::mem::take(&mut current));
2642 } else {
2643 current.clear();
2644 }
2645 continue;
2646 }
2647 let full = kept.len() == 1 && kept[0].0 <= 0.0 && kept[0].1 >= 1.0;
2648 let at = |t: f64| [p[0] + (q[0] - p[0]) * t, p[1] + (q[1] - p[1]) * t];
2649 if full {
2650 if current.is_empty() {
2651 current.push(p);
2652 }
2653 current.push(q);
2654 continue;
2655 }
2656 if !kept.is_empty() && kept[0].0 <= 0.0 {
2659 let (_, end) = kept.remove(0);
2660 if current.is_empty() {
2661 current.push(p);
2662 }
2663 current.push(at(end));
2664 }
2665 if current.len() >= 2 {
2666 out.push(std::mem::take(&mut current));
2667 } else {
2668 current.clear();
2669 }
2670 for (start, end) in kept {
2671 if end >= 1.0 {
2672 current = vec![at(start), q];
2673 } else {
2674 out.push(vec![at(start), at(end)]);
2675 }
2676 }
2677 }
2678 if current.len() >= 2 {
2679 out.push(current);
2680 }
2681 }
2682 out
2683}
2684
2685pub(super) fn break_for_text_boxes(
2689 lines: Vec<Vec<[f64; 2]>>,
2690 runs: &[SheetText],
2691) -> Vec<Vec<[f64; 2]>> {
2692 let boxes: Vec<Box2> = runs.iter().map(Box2::of_text).collect();
2693 break_for_text(lines, &boxes)
2694}
2695
2696fn visible_spans(p: [f64; 2], q: [f64; 2], boxes: &[Box2]) -> Vec<(f64, f64)> {
2699 let mut blocked: Vec<(f64, f64)> = Vec::new();
2700 for box2 in boxes {
2701 let (Some(a), Some(b)) = (box2.local(p), box2.local(q)) else {
2702 continue;
2703 };
2704 let (mut t0, mut t1) = (0.0f64, 1.0f64);
2706 let mut inside = true;
2707 for (axis, half) in [(0usize, box2.half[0]), (1usize, box2.half[1])] {
2708 let delta = b[axis] - a[axis];
2709 if delta.abs() < 1e-12 {
2710 if a[axis].abs() > half {
2711 inside = false;
2712 break;
2713 }
2714 continue;
2715 }
2716 let (lo, hi) = ((-half - a[axis]) / delta, (half - a[axis]) / delta);
2717 let (lo, hi) = if lo <= hi { (lo, hi) } else { (hi, lo) };
2718 t0 = t0.max(lo);
2719 t1 = t1.min(hi);
2720 if t0 > t1 {
2721 inside = false;
2722 break;
2723 }
2724 }
2725 if inside && t1 > t0 {
2726 blocked.push((t0, t1));
2727 }
2728 }
2729 if blocked.is_empty() {
2730 return vec![(0.0, 1.0)];
2731 }
2732 blocked.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
2733 let mut spans = Vec::new();
2734 let mut cursor = 0.0f64;
2735 for (start, end) in blocked {
2736 if start > cursor + 1e-9 {
2737 spans.push((cursor, start.min(1.0)));
2738 }
2739 cursor = cursor.max(end);
2740 if cursor >= 1.0 {
2741 break;
2742 }
2743 }
2744 if cursor < 1.0 - 1e-9 {
2745 spans.push((cursor, 1.0));
2746 }
2747 spans
2748}
2749
2750pub fn plane_is_edge_on(row: &PmiAnnotationReport, forward: [f64; 3]) -> bool {
2755 match row.plane.as_ref() {
2756 None => false,
2757 Some(plane) => dot(norm(plane.normal), forward).abs() < EDGE_ON_COS,
2758 }
2759}
2760
2761fn tolerance_sources(
2767 report: Option<&PmiViewReport>,
2768 view: &PmiView,
2769 frame: &ViewFrame,
2770 placed: &PlacedView,
2771 env: &brep_kernel::Env,
2772) -> Vec<ToleranceSource> {
2773 let Some(report) = report else { return Vec::new() };
2774 let project = |p: [f64; 3]| -> [f64; 2] {
2775 let v = frame.to_view(p);
2776 to_paper(placed, v[0], v[1])
2777 };
2778 let mut out = Vec::new();
2779 for row in report.annotations.iter().filter(|row| row.enabled && row.status == PmiStatus::Ok) {
2780 let Some(annotation) = view.annotations.iter().find(|annotation| annotation.id() == row.id) else { continue };
2781 let Ok(block) = brep_kernel::ToleranceBlock::read(annotation, env) else { continue };
2782 let reference = annotation.flag("isReference");
2783 if block.mode == brep_kernel::ToleranceMode::None && !reference {
2784 continue;
2785 }
2786 let measures = match &row.geometry {
2787 brep_kernel::PmiGeometry::Linear { a, b, component: None } => Measures::Linear { a: project(*a), b: project(*b) },
2788 brep_kernel::PmiGeometry::Radial { center, radius, diameter, sphere: false, .. } => {
2789 Measures::Radial { centre: project(*center), radius: *radius, diameter: *diameter }
2790 }
2791 _ => continue,
2792 };
2793 out.push(ToleranceSource { id: row.id.clone(), measures, block, reference });
2794 }
2795 out
2796}
2797
2798fn annotations(
2799 report: Option<&PmiViewReport>,
2800 view: &PmiView,
2801 frame: &ViewFrame,
2802 placed: &PlacedView,
2803) -> Vec<AnnotationDrawing> {
2804 let Some(report) = report else {
2805 return Vec::new();
2806 };
2807 let paper_text = view.display.text_size_pt * MM_PER_POINT;
2811 let text_height = paper_text / placed.scale.max(1e-9);
2812 let style = PmiLayoutStyle {
2813 arrow: text_height * ARROW_PER_TEXT,
2814 text_height,
2815 view_dir: frame.forward,
2816 view_up: frame.up,
2817 plane: None,
2818 };
2819 let flatten = placed.flatten_text;
2820 let mut out = Vec::new();
2821 for row in &report.annotations {
2822 if !row.enabled || row.status != PmiStatus::Ok {
2823 continue;
2824 }
2825 let row_style = PmiLayoutStyle { plane: row.plane, ..style };
2826 let drawn = pmi_present(&row.geometry, row.label_world, &row.text, &row_style);
2827 let project = |p: [f64; 3]| -> [f64; 2] {
2830 let v = frame.to_view(p);
2831 to_paper(placed, v[0], v[1])
2832 };
2833 let project_line = |polyline: &Vec<[f64; 3]>| -> Vec<[f64; 2]> {
2834 polyline.iter().map(|p| project(*p)).collect()
2835 };
2836 let mut lines: Vec<Vec<[f64; 2]>> = drawn.polylines.iter().map(project_line).collect();
2837 for arrow in &drawn.arrows {
2840 let mut closed: Vec<[f64; 3]> = arrow.to_vec();
2841 closed.push(arrow[0]);
2842 lines.push(project_line(&closed));
2843 }
2844
2845 let unit = drawn
2852 .texts
2853 .first()
2854 .filter(|_| !drawn.frames.is_empty())
2855 .map(|run| Unit {
2856 anchor: drawn.text_anchor,
2857 right: run.right,
2858 up: run.up,
2859 cap: run.height,
2860 });
2861 let flat_about = |anchor: [f64; 3], right3: [f64; 3], up3: [f64; 3], p: [f64; 3]| -> [f64; 2] {
2866 let d = sub(p, anchor);
2867 let a = project(anchor);
2868 [a[0] + dot(d, right3) * placed.scale, a[1] - dot(d, up3) * placed.scale]
2869 };
2870
2871 let explode = matches!(row.geometry, brep_kernel::PmiGeometry::Explode { .. });
2881 let texts: Vec<SheetText> = drawn
2882 .texts
2883 .iter()
2884 .filter(|run| !explode && !run.text.trim().is_empty())
2885 .map(|run| {
2886 let cap = run.height * placed.scale;
2887 if flatten {
2888 let anchor = match unit {
2892 Some(unit) => flat_about(unit.anchor, unit.right, unit.up, run.anchor),
2893 None => project(run.anchor),
2894 };
2895 return SheetText::flat(run.text.clone(), anchor, cap);
2896 }
2897 let image = |v: [f64; 3]| -> [f64; 2] {
2899 [dot(v, frame.right) * cap, -dot(v, frame.up) * cap]
2900 };
2901 let mut right = image(run.right);
2902 if dot(cross(run.right, run.up), frame.forward) > 0.0 {
2907 right = [-right[0], -right[1]];
2908 }
2909 SheetText::new(run.text.clone(), project(run.anchor), right, image(run.up))
2910 })
2911 .collect();
2912
2913 let frames: Vec<Vec<[f64; 2]>> = match (flatten, unit) {
2916 (true, Some(unit)) => drawn
2917 .frames
2918 .iter()
2919 .map(|polyline| {
2920 polyline
2921 .iter()
2922 .map(|p| flat_about(unit.anchor, unit.right, unit.up, *p))
2923 .collect()
2924 })
2925 .collect(),
2926 _ => drawn.frames.iter().map(project_line).collect(),
2927 };
2928 let mut boxes: Vec<Box2> = texts.iter().map(Box2::of_text).collect();
2932 if let Some(unit) = unit {
2933 let scaled = unit.cap * placed.scale;
2934 let (right, up) = if flatten {
2935 ([scaled, 0.0], [0.0, -scaled])
2936 } else {
2937 let image = |v: [f64; 3]| [dot(v, frame.right) * scaled, -dot(v, frame.up) * scaled];
2938 (image(unit.right), image(unit.up))
2939 };
2940 if let Some(box2) = unit.box_of(&drawn.frames, project(unit.anchor), right, up) {
2941 boxes.push(box2);
2942 }
2943 }
2944 let mut lines = break_for_text(lines, &boxes);
2945 lines.extend(frames);
2946
2947 out.push(AnnotationDrawing {
2948 id: row.id.clone(),
2949 kind: row.kind.clone(),
2950 text: row.text.clone(),
2951 lines,
2952 texts,
2953 note: if !plane_is_edge_on(row, frame.forward) {
2954 String::new()
2955 } else if flatten {
2956 format!(
2957 "annotation plane edge-on to '{}': its geometry projects to a line, its text is flattened onto the sheet",
2958 view.name
2959 )
2960 } else {
2961 format!(
2962 "annotation plane edge-on to '{}': it and its text project to a line",
2963 view.name
2964 )
2965 },
2966 });
2967 }
2968 out
2969}
2970
2971#[derive(Debug, Clone, Copy)]
2977struct Unit {
2978 anchor: [f64; 3],
2979 right: [f64; 3],
2980 up: [f64; 3],
2981 cap: f64,
2982}
2983
2984impl Unit {
2985 fn box_of(
2991 &self,
2992 frames: &[Vec<[f64; 3]>],
2993 anchor: [f64; 2],
2994 right: [f64; 2],
2995 up: [f64; 2],
2996 ) -> Option<Box2> {
2997 if self.cap <= 0.0 {
2998 return None;
2999 }
3000 let mut lo = [f64::INFINITY; 2];
3001 let mut hi = [f64::NEG_INFINITY; 2];
3002 for polyline in frames {
3003 for p in polyline {
3004 let d = sub(*p, self.anchor);
3005 let local = [dot(d, self.right) / self.cap, dot(d, self.up) / self.cap];
3006 for axis in 0..2 {
3007 lo[axis] = lo[axis].min(local[axis]);
3008 hi[axis] = hi[axis].max(local[axis]);
3009 }
3010 }
3011 }
3012 if lo[0] > hi[0] {
3013 return None;
3014 }
3015 let mid = [(lo[0] + hi[0]) * 0.5, (lo[1] + hi[1]) * 0.5];
3016 Some(Box2 {
3017 centre: [
3018 anchor[0] + mid[0] * right[0] + mid[1] * up[0],
3019 anchor[1] + mid[0] * right[1] + mid[1] * up[1],
3020 ],
3021 right,
3022 up,
3023 half: [(hi[0] - lo[0]) * 0.5, (hi[1] - lo[1]) * 0.5],
3024 })
3025 }
3026}
3027
3028
3029
3030