1use geop_core_editor::selectable::Selectable;
10use geop_core_geometry::{
11 aabb::union_aabb,
12 intersection::curve_curve_overlaps_and_crossings,
13 nurb_curve::{NurbCurve2D, NurbCurve3D},
14 search::{MAX_NODES, min_subdivision_size},
15 shape::{Arc, Circle},
16};
17use geop_core_math::{
18 geop_error::{GeopError, GeopResult, WithContext},
19 plain::{self, add, normalize, sub},
20 primitives::Pose,
21 scalars::Scalar,
22 solvers::{
23 probe::{Probe, attempts},
24 repeat::{Step, repeat},
25 },
26 vector::{Vector2, Vector3},
27};
28use geop_core_program::{placed::Located, reader::Reader};
29use geop_core_sketch::dimension::Measure;
30use geop_core_topology::{FaceId, Model};
31use geop_lib_sheet::{Anchor, Layer, P, Shape, Sheet, decimal, strokes_of};
32use geop_ops_model::placed::locate;
33use geop_ops_model::sketch::SketchPlane;
34use geop_ops_model::{CosmeticThread, Edge, NamedModel, Vertex, element, frame_along};
35use serde::{Deserialize, Serialize};
36
37use crate::{
38 annotation::{Annotation, Candidate, Drawn, candidates},
39 hidden_lines::{LineKind, ProjectedView, ViewOptions},
40 layer,
41 scene::{Body, Scene},
42 section::{cut_faces, section_part},
43 view::{DrawnView, ViewFrame, ViewKind},
44};
45
46#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "snake_case")]
49pub enum Projection {
50 #[default]
53 ThirdAngle,
54 FirstAngle,
57}
58
59impl Projection {
60 pub fn label(self) -> &'static str {
61 match self {
62 Projection::ThirdAngle => "THIRD ANGLE",
63 Projection::FirstAngle => "FIRST ANGLE",
64 }
65 }
66}
67
68#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
70#[serde(rename_all = "snake_case")]
71pub enum SheetSize {
72 A4,
73 #[default]
74 A3,
75 A2,
76 A1,
77 A0,
78}
79
80impl SheetSize {
81 pub const ALL: [SheetSize; 5] = [
82 SheetSize::A4,
83 SheetSize::A3,
84 SheetSize::A2,
85 SheetSize::A1,
86 SheetSize::A0,
87 ];
88
89 pub fn name(self) -> &'static str {
90 match self {
91 SheetSize::A4 => "a4",
92 SheetSize::A3 => "a3",
93 SheetSize::A2 => "a2",
94 SheetSize::A1 => "a1",
95 SheetSize::A0 => "a0",
96 }
97 }
98
99 pub fn size(self) -> (f64, f64) {
101 match self {
102 SheetSize::A4 => (297.0, 210.0),
103 SheetSize::A3 => (420.0, 297.0),
104 SheetSize::A2 => (594.0, 420.0),
105 SheetSize::A1 => (841.0, 594.0),
106 SheetSize::A0 => (1189.0, 841.0),
107 }
108 }
109}
110
111#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
113pub struct DrawingArgs {
114 #[serde(default = "default_views")]
115 pub views: Vec<ViewKind>,
116 #[serde(default)]
117 pub projection: Projection,
118 #[serde(default)]
119 pub sheet: SheetSize,
120 #[serde(default, skip_serializing_if = "Option::is_none")]
123 pub scale: Option<f64>,
124 #[serde(default)]
125 pub tangent_edges: bool,
126 #[serde(default = "yes")]
127 pub hidden_lines: bool,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub section: Option<SketchPlane>,
132 #[serde(default, skip_serializing_if = "Vec::is_empty")]
135 pub annotations: Vec<Annotation>,
136 #[serde(default)]
138 pub name: String,
139 #[serde(default)]
141 pub material: String,
142 #[serde(default)]
147 pub bom: bool,
148}
149
150#[derive(Clone, Debug, Default, PartialEq)]
158pub struct PartsListLine {
159 pub item: String,
160 pub quantity: u64,
161 pub name: String,
162 pub designation: String,
163 pub material: String,
164 pub placements: Vec<String>,
165}
166
167fn default_views() -> Vec<ViewKind> {
168 vec![
169 ViewKind::Front,
170 ViewKind::Top,
171 ViewKind::Right,
172 ViewKind::Iso,
173 ]
174}
175
176fn yes() -> bool {
177 true
178}
179
180impl Default for DrawingArgs {
181 fn default() -> Self {
182 DrawingArgs {
183 views: default_views(),
184 projection: Projection::default(),
185 sheet: SheetSize::default(),
186 scale: None,
187 tangent_edges: false,
188 hidden_lines: true,
189 section: None,
190 annotations: Vec::new(),
191 name: String::new(),
192 material: String::new(),
193 bom: false,
194 }
195 }
196}
197
198pub const SCALES: [f64; 15] = [
200 50.0, 20.0, 10.0, 5.0, 2.0, 1.0, 0.5, 0.2, 0.1, 0.05, 0.02, 0.01, 0.005, 0.002, 0.001,
201];
202const MARGIN: f64 = 10.0;
204const GAP: f64 = 28.0;
206const TITLE_WIDTH: f64 = 180.0;
208const TITLE_HEIGHT: f64 = 40.0;
209pub(crate) const TEXT: f64 = 3.5;
211const TITLE_TEXT: f64 = 7.0;
212pub(crate) const ARROW_LENGTH: f64 = 3.0;
214pub(crate) const ARROW_WIDTH: f64 = 1.0;
215pub(crate) const DIMENSION_OFFSET: f64 = 10.0;
217pub(crate) const CENTER_OVERSHOOT: f64 = 3.0;
219const HATCH_SPACING: f64 = 3.0;
221const LIST_ROW: f64 = 6.0;
224const LIST_COLUMNS: [(&str, f64); 5] = [
225 ("ITEM", 14.0),
226 ("QTY", 12.0),
227 ("NAME", 54.0),
228 ("DESIGNATION", 60.0),
229 ("MATERIAL", 40.0),
230];
231const LIST_TEXT: f64 = 2.5;
232const BALLOON_RADIUS: f64 = 4.0;
236const BALLOON_OFFSET: f64 = 22.0;
237const BALLOON_MARGIN: f64 = BALLOON_OFFSET + BALLOON_RADIUS;
238const LEADER_DOT: f64 = 0.5;
239
240pub fn scale_label(scale: f64) -> String {
242 if scale >= 1.0 {
243 format!("{}:1", length_label(scale))
244 } else {
245 format!("1:{}", length_label(1.0 / scale))
246 }
247}
248
249pub fn length_label(x: f64) -> String {
252 decimal(x, 2)
253}
254
255pub(crate) struct Placed {
257 pub(crate) kind: DrawnView,
258 pub(crate) view: ProjectedView,
259 hatched: Vec<Hatched>,
262 pub(crate) extents: [f64; 4],
264 pub(crate) center: P,
266 margin: f64,
269}
270
271fn cell(view: DrawnView, projection: Projection) -> (usize, usize) {
274 let third = projection == Projection::ThirdAngle;
275 match view {
276 DrawnView::View(ViewKind::Front) => (1, 1),
277 DrawnView::View(ViewKind::Top) => (1, if third { 0 } else { 2 }),
278 DrawnView::View(ViewKind::Bottom) => (1, if third { 2 } else { 0 }),
279 DrawnView::View(ViewKind::Right) => (if third { 2 } else { 0 }, 1),
280 DrawnView::View(ViewKind::Left) => (if third { 0 } else { 2 }, 1),
281 DrawnView::View(ViewKind::Back) => (3, 1),
282 DrawnView::View(ViewKind::Iso) => (if third { 2 } else { 0 }, if third { 0 } else { 2 }),
283 DrawnView::Section => (if third { 2 } else { 0 }, if third { 2 } else { 0 }),
284 }
285}
286
287pub fn drawn_faces(model: &Model) -> Vec<FaceId> {
290 let mut faces: Vec<FaceId> = model.faces.keys().copied().collect();
291 faces.sort_by_key(|f| f.0);
292 faces
293}
294
295pub fn compose(
300 view: &Reader,
301 args: &DrawingArgs,
302 date: &str,
303 parts: &[PartsListLine],
304) -> GeopResult<Sheet> {
305 annotated(view, args, &layout(view, args, date, parts)?)
306}
307
308pub fn annotated(view: &Reader, args: &DrawingArgs, layout: &Layout) -> GeopResult<Sheet> {
311 let mut sheet = layout.sheet.clone();
312 for (index, annotation) in args.annotations.iter().enumerate() {
313 annotation
314 .drawn(view, layout)
315 .map_err(|e| {
316 e.with_context(format!(
317 "annotation {}: {}",
318 index + 1,
319 annotation.describe()
320 ))
321 })?
322 .draw(&mut sheet);
323 }
324 Ok(sheet)
325}
326
327#[derive(Clone, Debug)]
332pub struct Layout {
333 pub sheet: Sheet,
334 pub(crate) views: Vec<Placement>,
335 pub scale: f64,
336 pub candidates: Vec<Candidate>,
337}
338
339#[derive(Clone, Copy, Debug)]
342pub(crate) struct Placement {
343 pub(crate) view: DrawnView,
344 mid: P,
345 center: P,
346}
347
348impl Placement {
349 pub(crate) fn of(p: &Placed) -> Self {
350 Placement {
351 view: p.kind,
352 mid: [
353 (p.extents[0] + p.extents[2]) / 2.0,
354 (p.extents[1] + p.extents[3]) / 2.0,
355 ],
356 center: p.center,
357 }
358 }
359
360 pub(crate) fn place(&self, scale: f64, q: P) -> P {
363 [
364 self.center[0] + scale * (q[0] - self.mid[0]),
365 self.center[1] + scale * (q[1] - self.mid[1]),
366 ]
367 }
368}
369
370impl Layout {
371 pub(crate) fn placement(&self, view: DrawnView) -> GeopResult<&Placement> {
373 self.views.iter().find(|p| p.view == view).ok_or_else(|| {
374 GeopError::new(format!(
375 "the drawing has no {view} view any more: show it again, or remove what is in it"
376 ))
377 })
378 }
379}
380
381pub fn layout(
386 view: &Reader,
387 args: &DrawingArgs,
388 date: &str,
389 parts: &[PartsListLine],
390) -> GeopResult<Layout> {
391 arrange(view, args, date, parts, &project(view, args)?)
392}
393
394#[derive(Clone)]
400pub struct Projected {
401 views: Vec<(DrawnView, ProjectedView, Vec<Hatched>)>,
402}
403
404impl Projected {
405 pub fn depends_on(args: &DrawingArgs) -> DrawingArgs {
408 DrawingArgs {
409 views: args.views.clone(),
410 hidden_lines: args.hidden_lines,
411 tangent_edges: args.tangent_edges,
412 section: args.section.clone(),
413 ..DrawingArgs::default()
414 }
415 }
416}
417
418pub fn project(view: &Reader, args: &DrawingArgs) -> GeopResult<Projected> {
422 let ctx = |e: GeopError| e.with_context("project(drawing)");
423 let scene = Scene::of(view).with_context(&ctx)?;
424 let options = ViewOptions {
425 tangent_edges: args.tangent_edges,
426 hidden_lines: args.hidden_lines,
427 };
428 let mut views = Vec::new();
429 if scene.bodies.is_empty() {
430 return Ok(Projected { views });
431 }
432 for &kind in &args.views {
433 if views.iter().any(|(v, _, _)| *v == DrawnView::View(kind)) {
434 continue;
435 }
436 let frame = kind.frame()?;
437 let view = scene
438 .project(&frame, &options)
439 .map_err(|e| ctx(e.with_context(format!("the {} view", kind.name()))))?;
440 views.push((DrawnView::View(kind), view, Vec::new()));
441 }
442 if let Some(plane) = &args.section {
443 let (section, hatched) = section_view(view, &scene, plane, &options).map_err(|e| {
444 ctx(e.with_context(format!("the section view on {}", plane.picked().label())))
445 })?;
446 views.push((DrawnView::Section, section, hatched));
447 }
448 Ok(Projected { views })
449}
450
451pub fn arrange(
459 view: &Reader,
460 args: &DrawingArgs,
461 date: &str,
462 parts: &[PartsListLine],
463 projected: &Projected,
464) -> GeopResult<Layout> {
465 let ctx = |e: GeopError| e.with_context("arrange(drawing)");
466 let (width, height) = args.sheet.size();
467 let list_height = match args.bom {
468 true => LIST_ROW * (parts.len() + 1) as f64,
469 false => 0.0,
470 };
471 if list_height > height - 2.0 * MARGIN - TITLE_HEIGHT {
472 return Err(GeopError::new(format!(
473 "the bill of materials has {} lines, too many for an {} sheet: choose a larger one",
474 parts.len(),
475 args.sheet.name().to_uppercase()
476 )));
477 }
478 let scene = Scene::of(view).with_context(&ctx)?;
479 if scene.bodies.is_empty() && !args.bom {
480 return Err(GeopError::new(
481 "the part has nothing to draw: it has no faces",
482 ));
483 }
484 let options = ViewOptions {
485 tangent_edges: args.tangent_edges,
486 hidden_lines: args.hidden_lines,
487 };
488 let mut placed: Vec<Placed> = projected
489 .views
490 .iter()
491 .filter_map(|(kind, view, hatched)| {
492 let e = view.extents()?;
493 Some(Placed {
494 kind: *kind,
495 extents: [e[0], e[1], e[2], e[3]].map(|x| x.to_f64()),
496 view: view.clone(),
497 hatched: hatched.clone(),
498 center: [0.0, 0.0],
499 margin: 0.0,
500 })
501 })
502 .collect();
503 if placed.is_empty() && !args.bom {
504 return Err(GeopError::new("no view of the drawing shows any line"));
505 }
506 let mut sheet = Sheet {
507 width,
508 height,
509 layers: layer::ALL.to_vec(),
510 ..Sheet::default()
511 };
512 if placed.is_empty() {
513 let scale = args.scale.unwrap_or(1.0);
515 draw_frame(&mut sheet, args, scale, date);
516 draw_parts_list(&mut sheet, parts);
517 return Ok(Layout {
518 sheet,
519 views: Vec::new(),
520 scale,
521 candidates: Vec::new(),
522 });
523 }
524 let balloons = match args.bom && scene.bodies.iter().any(|b| !b.path.is_empty()) {
525 true => balloon_view(&scene, &placed, parts)?,
526 false => None,
527 };
528 if let Some((index, _)) = &balloons {
529 placed[*index].margin = BALLOON_MARGIN;
530 }
531
532 let mut columns: Vec<(usize, f64, f64)> = Vec::new();
536 let mut rows: Vec<(usize, f64, f64)> = Vec::new();
537 for p in &placed {
538 let (c, r) = cell(p.kind, args.projection);
539 let (w, h) = (p.extents[2] - p.extents[0], p.extents[3] - p.extents[1]);
540 grow(&mut columns, c, w, p.margin);
541 grow(&mut rows, r, h, p.margin);
542 }
543 columns.sort_by_key(|&(c, _, _)| c);
544 rows.sort_by_key(|&(r, _, _)| r);
545 let model_width: f64 = columns.iter().map(|&(_, w, _)| w).sum();
546 let model_height: f64 = rows.iter().map(|&(_, h, _)| h).sum();
547 let paper_width: f64 = columns.iter().map(|&(_, _, m)| 2.0 * m).sum();
548 let paper_height: f64 = rows.iter().map(|&(_, _, m)| 2.0 * m).sum();
549 let area_width = width - 2.0 * MARGIN;
550 let area_height = height - 2.0 * MARGIN - TITLE_HEIGHT - list_height;
551 let fit = ((area_width - paper_width - GAP * (columns.len() + 1) as f64)
552 / model_width.max(1e-300))
553 .min((area_height - paper_height - GAP * (rows.len() + 1) as f64) / model_height.max(1e-300));
554 let scale = match args.scale {
555 Some(s) if s.is_finite() && s > 0.0 => s,
556 Some(s) => {
557 return Err(GeopError::new(format!(
558 "the scale {s} is not a positive number"
559 )));
560 }
561 None => SCALES
562 .iter()
563 .copied()
564 .find(|&s| s <= fit)
565 .unwrap_or(SCALES[SCALES.len() - 1]),
566 };
567 let used_width = scale * model_width + paper_width + GAP * (columns.len() - 1) as f64;
569 let used_height = scale * model_height + paper_height + GAP * (rows.len() - 1) as f64;
570 let left = MARGIN + (area_width - used_width) / 2.0;
571 let top = height - MARGIN - (area_height - used_height) / 2.0;
572 for p in &mut placed {
573 let (c, r) = cell(p.kind, args.projection);
574 let ci = columns.iter().position(|&(k, _, _)| k == c).unwrap_or(0);
575 let ri = rows.iter().position(|&(k, _, _)| k == r).unwrap_or(0);
576 let x: f64 = left
577 + columns[..ci]
578 .iter()
579 .map(|&(_, w, m)| scale * w + 2.0 * m + GAP)
580 .sum::<f64>()
581 + columns[ci].2
582 + scale * columns[ci].1 / 2.0;
583 let y: f64 = top
584 - rows[..ri]
585 .iter()
586 .map(|&(_, h, m)| scale * h + 2.0 * m + GAP)
587 .sum::<f64>()
588 - rows[ri].2
589 - scale * rows[ri].1 / 2.0;
590 p.center = [x, y];
591 }
592
593 for p in &placed {
594 draw_view(&mut sheet, p, scale).with_context(&ctx)?;
595 }
596 draw_threads(&mut sheet, &scene, &placed, scale, &options).with_context(&ctx)?;
597 if let Some((index, targets)) = &balloons {
598 draw_balloons(&mut sheet, &placed[*index], targets, scale).with_context(&ctx)?;
599 }
600 draw_frame(&mut sheet, args, scale, date);
601 if args.bom {
602 draw_parts_list(&mut sheet, parts);
603 }
604 let candidates = candidates(&scene, &placed, scale).with_context(&ctx)?;
605 Ok(Layout {
606 sheet,
607 views: placed.iter().map(Placement::of).collect(),
608 scale,
609 candidates,
610 })
611}
612
613type Target = (String, P);
616
617fn balloon_view(
630 scene: &Scene<'_>,
631 placed: &[Placed],
632 parts: &[PartsListLine],
633) -> GeopResult<Option<(usize, Vec<Target>)>> {
634 let mut line_of: Vec<Option<usize>> = vec![None; scene.bodies.len()];
636 let paths: std::collections::HashMap<&str, usize> = parts
637 .iter()
638 .enumerate()
639 .flat_map(|(k, line)| line.placements.iter().map(move |p| (p.as_str(), k)))
640 .collect();
641 for (b, body) in scene.bodies.iter().enumerate() {
642 line_of[b] = paths.get(body.path.as_str()).copied();
643 }
644 let mut order: Vec<usize> = (0..placed.len())
645 .filter(|&i| matches!(placed[i].kind, DrawnView::View(_)))
646 .collect();
647 order.sort_by_key(|&i| placed[i].kind != DrawnView::View(ViewKind::Iso));
648 let mut best: Option<(usize, usize, Vec<Target>)> = None;
649 for i in order {
650 let p = &placed[i];
651 let middle = [
652 (p.extents[0] + p.extents[2]) / 2.0,
653 (p.extents[1] + p.extents[3]) / 2.0,
654 ];
655 let mut found: Vec<Option<(bool, f64, P)>> = vec![None; parts.len()];
658 for l in &p.view.lines {
659 let Some(k) = line_of[l.body] else { continue };
660 let (t0, t1) = l.curve.domain();
661 for f in [0.25, 0.5, 0.75] {
662 let q = l
663 .curve
664 .evaluate(Scalar::fraction(t0, t1, Scalar::from_f64(f)))?;
665 let q = [q[0].to_f64(), q[1].to_f64()];
666 let out = (q[0] - middle[0]).hypot(q[1] - middle[1]);
667 if found[k].is_none_or(|(visible, o, _)| (l.visible, out) > (visible, o)) {
668 found[k] = Some((l.visible, out, q));
669 }
670 }
671 }
672 let seen = found.iter().flatten().filter(|f| f.0).count();
673 let targets = found
674 .iter()
675 .zip(parts)
676 .filter_map(|(f, line)| f.map(|(_, _, q)| (line.item.clone(), q)))
677 .collect();
678 if best.as_ref().is_none_or(|(_, s, _)| seen > *s) {
679 best = Some((i, seen, targets));
680 }
681 }
682 Ok(best
683 .filter(|(_, _, targets)| !targets.is_empty())
684 .map(|(i, _, targets)| (i, targets)))
685}
686
687fn along_rectangle(r: [f64; 4], s: f64) -> P {
690 let (w, h) = (r[2] - r[0], r[3] - r[1]);
691 let s = s.rem_euclid(2.0 * (w + h));
692 if s < w {
693 [r[0] + s, r[1]]
694 } else if s < w + h {
695 [r[2], r[1] + s - w]
696 } else if s < 2.0 * w + h {
697 [r[2] - (s - w - h), r[3]]
698 } else {
699 [r[0], r[3] - (s - 2.0 * w - h)]
700 }
701}
702
703fn rectangle_exit(r: [f64; 4], from: P, d: P) -> f64 {
706 let (w, h) = (r[2] - r[0], r[3] - r[1]);
707 let mut best = (f64::INFINITY, 0.0);
709 let sides = [
712 (1, r[1], 0.0),
713 (0, r[2], w),
714 (1, r[3], w + h),
715 (0, r[0], 2.0 * w + h),
716 ];
717 for (k, &(axis, value, start)) in sides.iter().enumerate() {
718 if d[axis] == 0.0 {
719 continue;
720 }
721 let t = (value - from[axis]) / d[axis];
722 if t <= 0.0 || t >= best.0 {
723 continue;
724 }
725 let q = add(from, plain::scale(d, t));
726 let along = match k {
727 0 => q[0] - r[0],
728 1 => q[1] - r[1],
729 2 => r[2] - q[0],
730 _ => r[3] - q[1],
731 };
732 best = (t, start + along);
733 }
734 best.1
735}
736
737fn draw_balloons(sheet: &mut Sheet, p: &Placed, targets: &[Target], scale: f64) -> GeopResult<()> {
744 let place = placer(p, scale);
745 let [x0, y0, x1, y1] = p.extents;
746 let (lo, hi) = (place([x0, y0]), place([x1, y1]));
747 let ring = [
748 lo[0] - BALLOON_OFFSET,
749 lo[1] - BALLOON_OFFSET,
750 hi[0] + BALLOON_OFFSET,
751 hi[1] + BALLOON_OFFSET,
752 ];
753 let perimeter = 2.0 * (ring[2] - ring[0] + ring[3] - ring[1]);
754 let spacing = 2.0 * BALLOON_RADIUS + BALLOON_RADIUS / 2.0;
755 if targets.len() as f64 * spacing > perimeter {
756 return Err(GeopError::new(format!(
757 "{} balloons do not fit around the view: choose a larger sheet",
758 targets.len()
759 )));
760 }
761 let mut balloons: Vec<(f64, &Target)> = targets
762 .iter()
763 .map(|target| {
764 let d = sub(place(target.1), p.center);
765 let d = if d == [0.0, 0.0] { [0.0, 1.0] } else { d };
766 (rectangle_exit(ring, p.center, d), target)
767 })
768 .collect();
769 balloons.sort_by(|a, b| a.0.total_cmp(&b.0));
770 for k in 1..balloons.len() {
773 balloons[k].0 = balloons[k].0.max(balloons[k - 1].0 + spacing);
774 }
775 let n = balloons.len();
776 if n > 1 && balloons[n - 1].0 > balloons[0].0 + perimeter - spacing {
777 balloons[n - 1].0 = balloons[0].0 + perimeter - spacing;
778 for k in (0..n - 1).rev() {
779 balloons[k].0 = balloons[k].0.min(balloons[k + 1].0 - spacing);
780 }
781 }
782 for (s, (item, target)) in balloons {
783 let center = along_rectangle(ring, s);
784 let at = place(*target);
785 let toward = normalize(sub(at, center)).unwrap_or([1.0, 0.0]);
787 sheet.stroke(
788 layer::DIMENSION,
789 Shape::Circle {
790 center,
791 radius: BALLOON_RADIUS,
792 },
793 );
794 sheet.stroke(
795 layer::DIMENSION,
796 Shape::Line(add(center, plain::scale(toward, BALLOON_RADIUS)), at),
797 );
798 sheet.stroke(
799 layer::DIMENSION,
800 Shape::Circle {
801 center: at,
802 radius: LEADER_DOT,
803 },
804 );
805 sheet.label(
806 layer::DIMENSION,
807 [center[0], center[1] - TEXT / 2.0],
808 TEXT,
809 Anchor::Middle,
810 item.clone(),
811 );
812 }
813 Ok(())
814}
815
816fn draw_parts_list(sheet: &mut Sheet, parts: &[PartsListLine]) {
820 let x0 = sheet.width - MARGIN - TITLE_WIDTH;
821 let y0 = MARGIN + TITLE_HEIGHT;
822 let rows = parts.len() + 1;
823 let y1 = y0 + LIST_ROW * rows as f64;
824 for k in 1..=rows {
825 let y = y0 + LIST_ROW * k as f64;
826 sheet.stroke(layer::BORDER, Shape::Line([x0, y], [x0 + TITLE_WIDTH, y]));
827 }
828 let mut x = x0;
829 for (_, w) in LIST_COLUMNS {
830 sheet.stroke(layer::BORDER, Shape::Line([x, y0], [x, y1]));
831 x += w;
832 }
833 sheet.stroke(layer::BORDER, Shape::Line([x, y0], [x, y1]));
834 let row = |sheet: &mut Sheet, k: usize, cells: [&str; 5]| {
835 let mut x = x0;
836 for ((_, w), text) in LIST_COLUMNS.iter().zip(cells) {
837 let fits = ((w - 2.0) / (0.6 * LIST_TEXT)) as usize;
839 let text = match text.chars().count() > fits {
840 true => format!(
841 "{}…",
842 text.chars()
843 .take(fits.saturating_sub(1))
844 .collect::<String>()
845 ),
846 false => text.to_string(),
847 };
848 let at = [
849 x + 1.0,
850 y0 + LIST_ROW * k as f64 + (LIST_ROW - LIST_TEXT) / 2.0,
851 ];
852 sheet.label(layer::BORDER, at, LIST_TEXT, Anchor::Start, text);
853 x += w;
854 }
855 };
856 row(sheet, 0, LIST_COLUMNS.map(|(caption, _)| caption));
857 for (k, line) in parts.iter().enumerate() {
858 let quantity = line.quantity.to_string();
859 let cells = [
860 line.item.as_str(),
861 quantity.as_str(),
862 line.name.as_str(),
863 line.designation.as_str(),
864 line.material.as_str(),
865 ];
866 row(sheet, k + 1, cells);
867 }
868}
869
870enum ThreadSeen {
872 Side,
874 End,
876}
877
878fn draw_threads(
891 sheet: &mut Sheet,
892 scene: &Scene<'_>,
893 placed: &[Placed],
894 scale: f64,
895 options: &ViewOptions,
896) -> GeopResult<()> {
897 for body in &scene.bodies {
898 let mut threads: Vec<_> = body.threads.iter().flat_map(|t| t.iter()).collect();
899 threads.sort_by_key(|(name, _)| *name);
900 for (name, thread) in threads {
901 let ctx = |e: GeopError| match body.path.as_str() {
902 "" => e.with_context(format!("the cosmetic thread {name}")),
903 path => e.with_context(format!(
904 "the cosmetic thread {name} of the part placed as {path}"
905 )),
906 };
907 let (mut start, mut along) = (thread.axis.point, thread.axis.direction);
908 if let Some(pose) = &body.pose {
909 let motion = pose.motion();
910 start = motion.apply(&start);
911 along = motion.rotate(&along);
912 }
913 let seen = Seen {
914 scene,
915 placed,
916 scale,
917 options,
918 };
919 draw_thread(sheet, &seen, thread, start, along, body.path.is_empty())
920 .with_context(&ctx)?;
921 }
922 }
923 Ok(())
924}
925
926struct Seen<'a, 'p> {
929 scene: &'a Scene<'p>,
930 placed: &'a [Placed],
931 scale: f64,
932 options: &'a ViewOptions,
933}
934
935fn draw_thread(
939 sheet: &mut Sheet,
940 seen: &Seen<'_, '_>,
941 thread: &CosmeticThread,
942 start: Vector3,
943 along: Vector3,
944 label: bool,
945) -> GeopResult<()> {
946 let Seen {
947 scene,
948 placed,
949 scale,
950 options,
951 } = *seen;
952 let end = start.add(&along.prod_scalar(Scalar::from_f64(thread.length)));
953 let drawn = if thread.internal {
954 thread.major_diameter
955 } else {
956 thread.minor_diameter
957 } / 2.0;
958 let taper = thread.taper.to_f64();
960 let drawn_at = |along: f64| drawn + taper * along;
961 let mut labelled = !label;
962 for p in placed
963 .iter()
964 .filter(|p| matches!(p.kind, DrawnView::View(k) if k != ViewKind::Iso))
965 {
966 let frame = &p.view.frame;
967 let look = frame.direction.vector();
968 let seen = if look.prod_dot(&along).could_be_equal(Scalar::ZERO) {
969 ThreadSeen::Side
970 } else if look
971 .prod_cross(&along)
972 .to_array()
973 .iter()
974 .all(|c| c.could_be_equal(Scalar::ZERO))
975 {
976 ThreadSeen::End
977 } else {
978 continue;
979 };
980 let place = placer(p, scale);
981 let at = |q: &Vector3| {
982 let v = frame.project_point(q);
983 place([v[0].to_f64(), v[1].to_f64()])
984 };
985 let mut end_radius = drawn;
990 let (probe, label_at) = match seen {
991 ThreadSeen::Side => {
992 let half = thread.length / 2.0;
993 let middle = start.add(&along.prod_scalar(Scalar::from_f64(half)));
994 let radius = thread.radius.add(thread.taper.mul(Scalar::from_f64(half)));
995 (
996 middle.add(&frame.toward_eye().prod_scalar(radius)),
997 at(&end),
998 )
999 }
1000 ThreadSeen::End => {
1001 let (near, drawn) = if frame
1002 .direction
1003 .dot(&start)
1004 .definitely_greater(frame.direction.dot(&end))
1005 {
1006 (end, drawn_at(thread.length))
1007 } else {
1008 (start, drawn)
1009 };
1010 end_radius = drawn;
1011 let frame_there = frame_along(near, &along)?;
1012 let side = *frame_there.u();
1013 let c = at(&near);
1014 let reach = scale * drawn * std::f64::consts::FRAC_1_SQRT_2;
1015 (
1016 near.add(&side.prod_scalar(Scalar::from_f64(drawn))),
1017 [c[0] + reach, c[1] + reach],
1018 )
1019 }
1020 };
1021 let visible = scene.point_seen(frame, probe)?;
1022 if !visible && !options.hidden_lines {
1023 continue;
1024 }
1025 let layer = if visible {
1026 layer::THREAD
1027 } else {
1028 layer::HIDDEN
1029 };
1030 match seen {
1031 ThreadSeen::Side => {
1032 let side = look.prod_cross(&along).normalize()?;
1033 let off = |r: f64| side.prod_scalar(Scalar::from_f64(r));
1034 let (first, last) = (off(drawn), off(drawn_at(thread.length)));
1035 for (first, last) in [(first, last), (first.neg(), last.neg())] {
1036 sheet.stroke(
1037 layer,
1038 Shape::Line(at(&start.add(&first)), at(&end.add(&last))),
1039 );
1040 }
1041 }
1042 ThreadSeen::End => {
1043 sheet.stroke(
1046 layer,
1047 Shape::Arc {
1048 center: at(&start),
1049 radius: scale * end_radius,
1050 start: 100f64.to_radians(),
1051 end: 10f64.to_radians(),
1052 },
1053 );
1054 }
1055 }
1056 if !labelled {
1057 sheet.label(
1058 layer::DIMENSION,
1059 [label_at[0] + 1.5, label_at[1] + 1.5],
1060 TEXT,
1061 Anchor::Start,
1062 thread.designation.clone(),
1063 );
1064 labelled = true;
1065 }
1066 }
1067 Ok(())
1068}
1069
1070fn grow(cells: &mut Vec<(usize, f64, f64)>, key: usize, size: f64, margin: f64) {
1072 match cells.iter_mut().find(|(k, _, _)| *k == key) {
1073 Some((_, s, m)) => {
1074 *s = s.max(size);
1075 *m = m.max(margin);
1076 }
1077 None => cells.push((key, size, margin)),
1078 }
1079}
1080
1081pub(crate) fn placer(p: &Placed, scale: f64) -> impl Fn(P) -> P {
1084 let placement = Placement::of(p);
1085 move |q: P| placement.place(scale, q)
1086}
1087
1088fn draw_view(sheet: &mut Sheet, p: &Placed, scale: f64) -> GeopResult<()> {
1091 let place = placer(p, scale);
1092 let curves: Vec<(Layer, &NurbCurve2D)> = p
1093 .view
1094 .lines
1095 .iter()
1096 .map(|line| {
1097 let layer = if line.visible {
1098 layer::VISIBLE
1099 } else {
1100 layer::HIDDEN
1101 };
1102 (layer, &line.curve)
1103 })
1104 .collect();
1105 sheet.strokes.extend(strokes_of(&curves, &place, scale)?);
1106 for (boundary, mirrored) in &p.hatched {
1107 for (a, b) in hatch(boundary, scale, *mirrored)? {
1108 sheet.stroke(layer::HATCH, Shape::Line(place(a), place(b)));
1109 }
1110 }
1111 if p.kind.orthographic() {
1112 center_marks(sheet, p, scale)?;
1113 let [x0, y0, x1, y1] = p.extents;
1116 let (a, b) = (place([x0, y0]), place([x1, y0]));
1117 let below = [(a[0] + b[0]) / 2.0, a[1] - DIMENSION_OFFSET];
1118 Drawn::measure(
1119 Measure::Linear {
1120 a,
1121 b,
1122 along: [1.0, 0.0],
1123 },
1124 below,
1125 length_label(x1 - x0),
1126 )
1127 .draw(sheet);
1128 let (a, b) = (place([x1, y0]), place([x1, y1]));
1129 let right = [a[0] + DIMENSION_OFFSET, (a[1] + b[1]) / 2.0];
1130 Drawn::measure(
1131 Measure::Linear {
1132 a,
1133 b,
1134 along: [0.0, 1.0],
1135 },
1136 right,
1137 length_label(y1 - y0),
1138 )
1139 .draw(sheet);
1140 }
1141 if p.kind == DrawnView::Section {
1142 let [_, y0, _, _] = p.extents;
1143 let below = place([(p.extents[0] + p.extents[2]) / 2.0, y0]);
1144 sheet.label(
1145 layer::DIMENSION,
1146 [below[0], below[1] - DIMENSION_OFFSET - 2.5 * TEXT],
1147 TEXT * 1.4,
1148 Anchor::Middle,
1149 "SECTION A-A",
1150 );
1151 }
1152 Ok(())
1153}
1154
1155fn center_marks(sheet: &mut Sheet, p: &Placed, scale: f64) -> GeopResult<()> {
1159 let mut circles: Vec<(Vector3, Scalar, f64)> = Vec::new();
1161 for line in p.view.lines.iter().filter(|l| l.kind == LineKind::Edge) {
1162 let lifted = line.curve.lifted()?;
1163 let Some(arc) = arc_of(&lifted)? else {
1164 continue;
1165 };
1166 let sweep = arc.sweep();
1167 let (center, radius) = (arc.circle.center, arc.circle.radius);
1168 match circles
1169 .iter_mut()
1170 .find(|(c, r, _)| c.could_be_equal(¢er) && r.could_be_equal(radius))
1171 {
1172 Some((_, _, s)) => *s += sweep,
1173 None => circles.push((center, radius, sweep)),
1174 }
1175 }
1176 let mut marks: Vec<(Vector3, f64)> = Vec::new();
1177 for (center, radius, sweep) in circles {
1178 if sweep < std::f64::consts::PI * 0.999 {
1179 continue;
1180 }
1181 let r = radius.to_f64();
1182 match marks.iter_mut().find(|(c, _)| c.could_be_equal(¢er)) {
1183 Some((_, m)) => *m = m.max(r),
1184 None => marks.push((center, r)),
1185 }
1186 }
1187 let place = placer(p, scale);
1188 for (center, r) in marks {
1189 let c = place([center[0].to_f64(), center[1].to_f64()]);
1190 let reach = r * scale + CENTER_OVERSHOOT;
1191 sheet.stroke(
1192 layer::CENTER,
1193 Shape::Line([c[0] - reach, c[1]], [c[0] + reach, c[1]]),
1194 );
1195 sheet.stroke(
1196 layer::CENTER,
1197 Shape::Line([c[0], c[1] - reach], [c[0], c[1] + reach]),
1198 );
1199 }
1200 Ok(())
1201}
1202
1203fn owner<'a>(reader: &Reader<'a>, name: &str) -> GeopResult<(Reader<'a>, Option<Pose>, String)> {
1208 let located = Located::of_key(name)?;
1209 let (part, pose) = match located.path() {
1210 "" => (reader.clone(), None),
1211 path => (reader.placed_part(path)?, Some(locate(reader, path)?.world)),
1212 };
1213 Ok((part, pose, located.name().to_string()))
1214}
1215
1216pub(crate) fn placed_vertex(reader: &Reader, name: &str) -> GeopResult<Vector3> {
1219 let (part, pose, local) = owner(reader, name)?;
1220 let found = part.retrieve_element(&element::<Vertex>(local))?;
1221 let point = found.model.topology().get_vertex(found.id)?.point;
1222 Ok(match pose {
1223 Some(pose) => pose.apply(&point),
1224 None => point,
1225 })
1226}
1227
1228pub(crate) fn placed_edge(reader: &Reader, name: &str) -> GeopResult<NurbCurve3D> {
1231 let (part, pose, local) = owner(reader, name)?;
1232 let found = part.retrieve_element(&element::<Edge>(local))?;
1233 let curve = &found.model.topology().get_edge(found.id)?.curve;
1234 Ok(match pose {
1235 Some(pose) => curve.transform(&pose.motion()),
1236 None => curve.clone(),
1237 })
1238}
1239
1240pub(crate) fn arc_of(curve: &NurbCurve3D) -> GeopResult<Option<Arc>> {
1250 if let Some(arc) = curve.as_arc()? {
1251 return Ok(Some(arc));
1252 }
1253 if curve.as_line()?.is_some() {
1254 return Ok(None);
1255 }
1256 let (t0, t1) = curve.domain();
1257 let at = |f: f64| curve.evaluate(Scalar::fraction(t0, t1, Scalar::from_f64(f)).sharpen());
1258 let (start, end) = (curve.evaluate(t0)?, curve.evaluate(t1)?);
1259 let through = if start.could_be_equal(&end) {
1260 [start, at(1.0 / 3.0)?, at(2.0 / 3.0)?]
1261 } else {
1262 [start, at(0.5)?, end]
1263 };
1264 let Some(circle) = circle_through(through)? else {
1265 return Ok(None);
1266 };
1267 for f in [0.25, 0.75] {
1268 let off = at(f)?.sub(&circle.center).norm();
1269 if !off.could_be_equal(circle.radius) {
1270 return Ok(None);
1271 }
1272 }
1273 let arc = Arc { circle, start, end };
1274 let (overlaps, _) = curve_curve_overlaps_and_crossings(
1275 curve,
1276 &arc.to_curve()?,
1277 Scalar::ZERO,
1278 MAX_NODES,
1279 min_subdivision_size(),
1280 )?;
1281 let whole =
1282 (overlaps.iter()).any(|o| o.start.t.could_be_equal(t0) && o.end.t.could_be_equal(t1));
1283 Ok(whole.then_some(arc))
1284}
1285
1286fn circle_through([p, q, r]: [Vector3; 3]) -> GeopResult<Option<Circle>> {
1293 let origin = p.sharpen();
1294 let (p, q, r) = (p.sub(&origin), q.sub(&origin), r.sub(&origin));
1295 let (u, w) = (q.sub(&p), r.sub(&p));
1296 let n = u.prod_cross(&w);
1297 let n2 = n.norm_sq();
1298 if n2.could_be_equal(Scalar::ZERO) {
1299 return Ok(None);
1300 }
1301 let towards = w
1302 .prod_scalar(u.norm_sq())
1303 .sub(&u.prod_scalar(w.norm_sq()))
1304 .prod_cross(&n);
1305 let center = p.add(&towards.prod_scalar(Scalar::ONE.div(n2.mul(Scalar::TWO))?));
1306 Ok(Some(Circle {
1307 center: origin.add(¢er),
1308 normal: n.normalize()?,
1309 radius: p.sub(¢er).norm(),
1310 }))
1311}
1312
1313fn draw_frame(sheet: &mut Sheet, args: &DrawingArgs, scale: f64, date: &str) {
1315 let (w, h) = (sheet.width, sheet.height);
1316 let corners =
1317 |x0: f64, y0: f64, x1: f64, y1: f64| vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]];
1318 sheet.stroke(
1319 layer::BORDER,
1320 Shape::Polyline(corners(MARGIN, MARGIN, w - MARGIN, h - MARGIN)),
1321 );
1322 let (x0, y0) = (w - MARGIN - TITLE_WIDTH, MARGIN);
1323 let (x1, y1) = (w - MARGIN, MARGIN + TITLE_HEIGHT);
1324 sheet.stroke(layer::BORDER, Shape::Polyline(corners(x0, y0, x1, y1)));
1325 let row = TITLE_HEIGHT / 4.0;
1326 let half = x0 + TITLE_WIDTH / 2.0;
1327 for k in 1..4 {
1328 let y = y0 + row * k as f64;
1329 sheet.stroke(layer::BORDER, Shape::Line([x0, y], [x1, y]));
1330 }
1331 sheet.stroke(
1333 layer::BORDER,
1334 Shape::Line([half, y0], [half, y0 + 2.0 * row]),
1335 );
1336 let name = if args.name.is_empty() {
1337 "PART"
1338 } else {
1339 args.name.as_str()
1340 };
1341 let field = |sheet: &mut Sheet, x: f64, y: f64, caption: &str, value: &str| {
1342 sheet.label(
1343 layer::BORDER,
1344 [x + 2.0, y + row - 3.0],
1345 2.0,
1346 Anchor::Start,
1347 caption,
1348 );
1349 sheet.label(
1350 layer::BORDER,
1351 [x + 2.0, y + 1.5],
1352 TEXT,
1353 Anchor::Start,
1354 value,
1355 );
1356 };
1357 sheet.label(
1358 layer::BORDER,
1359 [x0 + 3.0, y0 + 3.0 * row + 1.5],
1360 TITLE_TEXT * 0.8,
1361 Anchor::Start,
1362 name,
1363 );
1364 let material = if args.material.is_empty() {
1365 "-"
1366 } else {
1367 args.material.as_str()
1368 };
1369 field(sheet, x0, y0 + 2.0 * row, "MATERIAL", material);
1370 field(sheet, x0, y0 + row, "SCALE", &scale_label(scale));
1371 field(sheet, half, y0 + row, "UNITS", "mm");
1372 field(sheet, x0, y0, "DATE", date);
1373 field(sheet, half, y0, "PROJECTION", args.projection.label());
1374}
1375
1376type Hatched = (Vec<NurbCurve2D>, bool);
1380
1381type Cut = (NamedModel, Vector3, Vector3);
1384
1385fn section_view(
1393 view: &Reader,
1394 scene: &Scene<'_>,
1395 plane: &SketchPlane,
1396 options: &ViewOptions,
1397) -> GeopResult<(ProjectedView, Vec<Hatched>)> {
1398 let frame_of = plane.resolve(view)?;
1399 let normal = frame_of.w().normalize()?;
1400 let origin = *frame_of.origin();
1401 if scene.bodies.iter().all(|b| b.model().solids.is_empty()) {
1402 return Err(GeopError::new("the part has no solid to cut"));
1403 }
1404 let up = if normal[2].to_f64().abs() < 0.9 {
1408 Vector3::axis(2)
1409 } else {
1410 normal.across()?[0]
1411 };
1412 let frame = ViewFrame::looking(normal.neg(), up)?;
1413 let mut cuts: Vec<(usize, Option<Cut>)> = Vec::new();
1416 for (k, body) in scene.bodies.iter().enumerate() {
1417 if body.model().solids.is_empty() {
1418 cuts.push((k, None));
1419 continue;
1420 }
1421 match scene.side_of(k, &origin, &normal) {
1422 Some(true) => continue,
1423 Some(false) => cuts.push((k, None)),
1424 None => {
1425 let (o, n) = match &body.pose {
1426 Some(pose) => {
1427 let back = pose.inverse().motion();
1428 (back.apply(&origin), back.rotate(&normal))
1429 }
1430 None => (origin, normal),
1431 };
1432 let cut = section_part(body.named, &o, &n).map_err(|e| {
1433 e.with_context(format!("cutting the part placed as {:?}", body.path))
1434 })?;
1435 cuts.push((k, Some((cut, o, n))));
1436 }
1437 }
1438 }
1439 let bodies = cuts
1440 .iter()
1441 .map(|(k, cut)| {
1442 let body = &scene.bodies[*k];
1443 match cut {
1444 None => Body {
1445 path: body.path.clone(),
1446 named: body.named,
1447 threads: body.threads,
1448 faces: body.faces.clone(),
1449 pose: body.pose,
1450 },
1451 Some((cut, _, _)) => Body {
1452 path: body.path.clone(),
1453 named: cut,
1454 threads: body.threads,
1455 faces: drawn_faces(cut.topology()),
1456 pose: body.pose,
1457 },
1458 }
1459 })
1460 .filter(|b| !b.faces.is_empty())
1461 .collect();
1462 let options = ViewOptions {
1463 hidden_lines: false,
1464 ..*options
1465 };
1466 let view = Scene::new(bodies)?.project(&frame, &options)?;
1467 let mut hatched = Vec::new();
1468 for (index, (k, (cut, o, n))) in cuts
1469 .iter()
1470 .filter_map(|(k, cut)| cut.as_ref().map(|c| (k, c)))
1471 .enumerate()
1472 {
1473 let model = cut.topology();
1474 let motion = scene.bodies[*k].pose.map(|p| p.motion());
1475 for face in cut_faces(model, &drawn_faces(model), o, n)? {
1476 let mut boundary = Vec::new();
1477 for edge in model.face_edges(face) {
1478 let curve = &model.get_edge(edge)?.curve;
1479 let curve = match &motion {
1480 Some(m) => curve.transform(m),
1481 None => curve.clone(),
1482 };
1483 boundary.push(frame.project_curve(&curve)?);
1484 }
1485 hatched.push((boundary, index % 2 == 1));
1486 }
1487 }
1488 Ok((view, hatched))
1489}
1490
1491fn hatch(boundary: &[NurbCurve2D], scale: f64, mirrored: bool) -> GeopResult<Vec<(P, P)>> {
1500 if mirrored {
1501 let flip = |c: &NurbCurve2D| {
1502 let points = c
1503 .control_points
1504 .iter()
1505 .map(|q| Vector3::from_array([q[0].neg(), q[1], q[2]]))
1506 .collect();
1507 NurbCurve2D::try_new(c.degree, points, c.knot_vector.clone())
1508 };
1509 let flipped = boundary.iter().map(flip).collect::<GeopResult<Vec<_>>>()?;
1510 let back = |p: P| [-p[0], p[1]];
1511 return Ok(hatch(&flipped, scale, false)?
1512 .into_iter()
1513 .map(|(a, b)| (back(a), back(b)))
1514 .collect());
1515 }
1516 let Some(bounds) = union_aabb(boundary.iter().map(|c| c.aabb())) else {
1517 return Ok(Vec::new());
1518 };
1519 let lo: [f64; 2] = std::array::from_fn(|k| bounds[k].lower().to_f64());
1520 let hi: [f64; 2] = std::array::from_fn(|k| bounds[k].upper().to_f64());
1521 if !(lo[0] < hi[0] && lo[1] < hi[1]) {
1522 return Ok(Vec::new());
1523 }
1524 let spacing = HATCH_SPACING / scale * std::f64::consts::SQRT_2;
1526 let (c_lo, c_hi) = (lo[0] - hi[1], hi[0] - lo[1]);
1527 let reach = (hi[0] - lo[0]) + (hi[1] - lo[1]) + 1.0;
1528 let mut out = Vec::new();
1529 let mut c = c_lo + spacing / 2.0;
1533 let lines = ((c_hi - c) / spacing).max(0.0).ceil() as usize;
1534 repeat(lines + 2, || -> GeopResult<_> {
1535 if c < c_hi {
1536 let shifts = [0.0, 0.37, -0.29];
1537 let line = attempts(shifts.len(), |&k| -> GeopResult<_> {
1538 let cc = c + shifts[k] * spacing;
1539 let x_start = lo[0] - 1.0;
1541 let a = [x_start, x_start - cc];
1542 let b = [x_start + reach, x_start + reach - cc];
1543 Ok(match hatch_line(boundary, a, b)? {
1544 Some(stretches) => Probe::Found(stretches),
1545 None => Probe::Next,
1546 })
1547 })?;
1548 if let Some(stretches) = line {
1549 out.extend(stretches);
1550 }
1551 c += spacing;
1552 Ok(Step::Again)
1553 } else {
1554 Ok(Step::Done(()))
1555 }
1556 })?
1557 .done("hatching across the face")?;
1558 Ok(out)
1559}
1560
1561fn hatch_line(boundary: &[NurbCurve2D], a: P, b: P) -> GeopResult<Option<Vec<(P, P)>>> {
1564 let point = |p: P| Vector2::from_array(p.map(Scalar::from_f64));
1565 let line = NurbCurve2D::line(point(a), point(b))?;
1566 let mut ts: Vec<f64> = Vec::new();
1567 for curve in boundary {
1568 let (overlaps, crossings) = curve_curve_overlaps_and_crossings(
1569 &line,
1570 curve,
1571 Scalar::ZERO,
1572 MAX_NODES,
1573 min_subdivision_size(),
1574 )?;
1575 if !overlaps.is_empty() {
1576 return Ok(None);
1577 }
1578 let (s0, s1) = curve.domain();
1579 for (t, s) in crossings {
1580 if s.could_be_equal(s0) || s.could_be_equal(s1) {
1581 return Ok(None);
1582 }
1583 ts.push(t.midpoint().to_f64());
1584 }
1585 }
1586 if ts.len() % 2 == 1 {
1587 return Ok(None);
1588 }
1589 ts.sort_by(f64::total_cmp);
1590 let at = |t: f64| [a[0] + t * (b[0] - a[0]), a[1] + t * (b[1] - a[1])];
1591 Ok(Some(
1592 ts.chunks(2)
1593 .map(|pair| (at(pair[0]), at(pair[1])))
1594 .collect(),
1595 ))
1596}