1use std::collections::HashMap;
30
31use geop_core_geometry::{
32 contains::curve::curve_could_contain,
33 intersection::{
34 curve_curve_overlaps_and_crossings, curve_surface_crossings, curve_surface_overlaps,
35 refine_crossing,
36 },
37 nurb_curve::{NurbCurve2D, NurbCurve3D},
38 nurb_surface::NurbSurface3D,
39};
40use geop_core_math::{
41 geop_error::{GeopError, GeopResult, WithContext},
42 plain::diagonal,
43 scalars::Scalar,
44 vector::{Vector2, Vector3},
45};
46use geop_core_topology::{
47 CoedgeGeometry, CoedgeId, EdgeId, FaceId, Model,
48 contains::face::{PointClassification, face_contains},
49};
50
51use crate::{
52 silhouette::{Silhouette, edge_crossings, face_silhouettes},
53 view::ViewFrame,
54};
55use geop_core_geometry::search::{MAX_NODES, PROJECT_ITERATIONS, min_subdivision_size};
56
57const FACE_CONTAINS_SEED: u64 = 0xD_0A_11;
59pub(crate) const COARSE_SUBDIVISION: f64 = 1e-5;
65
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68pub enum LineKind {
69 Edge,
71 Tangent,
73 Silhouette,
75}
76
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
79pub struct ViewOptions {
80 pub tangent_edges: bool,
82 pub hidden_lines: bool,
84}
85
86impl Default for ViewOptions {
87 fn default() -> Self {
88 ViewOptions {
89 tangent_edges: false,
90 hidden_lines: true,
91 }
92 }
93}
94
95#[derive(Clone, Debug)]
97pub struct ViewLine {
98 pub curve: NurbCurve2D,
100 pub curve3: NurbCurve3D,
102 pub kind: LineKind,
103 pub visible: bool,
104 pub edge: Option<EdgeId>,
106 pub body: usize,
109 pub(crate) source: usize,
111 pub(crate) range: (Scalar, Scalar),
112}
113
114#[derive(Clone, Debug)]
116pub struct ProjectedView {
117 pub frame: ViewFrame,
118 pub lines: Vec<ViewLine>,
119}
120
121impl ProjectedView {
122 pub fn extents(&self) -> Option<[Scalar; 4]> {
126 let mut b: Option<[Scalar; 4]> = None;
127 for line in &self.lines {
128 let (t0, t1) = line.curve.domain();
129 for t in [t0, t1] {
130 let Ok(p) = line.curve.evaluate(t) else {
131 continue;
132 };
133 let (x, y) = (p[0], p[1]);
134 b = Some(match b {
135 None => [x, y, x, y],
136 Some([a, c, e, f]) => [a.min(x), c.min(y), e.max(x), f.max(y)],
137 });
138 }
139 }
140 b
141 }
142}
143
144struct Source {
146 curve3: NurbCurve3D,
147 curve: NurbCurve2D,
148 kind: LineKind,
149 edge: Option<EdgeId>,
150 silhouette: Option<Silhouette>,
151 on: Vec<FaceId>,
153 origin: usize,
156 bounds: [f64; 4],
159}
160
161pub(crate) fn bounds(curve: &NurbCurve2D) -> [f64; 4] {
163 let [x, y, _] = curve.aabb();
164 [
165 x.lower().to_f64(),
166 y.lower().to_f64(),
167 x.upper().to_f64(),
168 y.upper().to_f64(),
169 ]
170}
171
172pub(crate) fn boxes_meet(a: &[f64; 4], b: &[f64; 4]) -> bool {
176 let r = COARSE_SUBDIVISION;
177 a[0] <= b[2] + r && b[0] <= a[2] + r && a[1] <= b[3] + r && b[1] <= a[3] + r
178}
179
180fn is_point(curve: &NurbCurve2D) -> GeopResult<bool> {
183 let first = curve.evaluate(curve.domain().0)?;
184 for q in &curve.control_points {
185 if !q.dehomogenized()?.could_be_equal(&first) {
186 return Ok(false);
187 }
188 }
189 Ok(true)
190}
191
192fn edge_coedges(model: &Model, faces: &[FaceId]) -> Vec<(EdgeId, Vec<CoedgeId>)> {
194 let mut order = Vec::new();
195 let mut map: HashMap<EdgeId, Vec<CoedgeId>> = HashMap::new();
196 for &face_id in faces {
197 for coedge_id in model.iterate_face_coedges(face_id) {
198 if let CoedgeGeometry::Edge(edge_id) = model.coedges[&coedge_id].geometry {
199 let entry = map.entry(edge_id).or_default();
200 if entry.is_empty() {
201 order.push(edge_id);
202 }
203 entry.push(coedge_id);
204 }
205 }
206 }
207 order
208 .into_iter()
209 .map(|e| {
210 let coedges = map.remove(&e).unwrap_or_default();
211 (e, coedges)
212 })
213 .collect()
214}
215
216fn edge_kind(
220 model: &Model,
221 edge_id: EdgeId,
222 coedges: &[CoedgeId],
223 d: &Vector3,
224) -> GeopResult<LineKind> {
225 let [a, b] = coedges else {
226 return Ok(LineKind::Edge);
227 };
228 let curve = &model.get_edge(edge_id)?.curve;
229 let point = curve.evaluate(curve.mid_parameter()?)?;
230 let normal = |coedge_id: CoedgeId| -> GeopResult<Vector3> {
231 let coedge = model.get_coedge(coedge_id)?;
232 let surface = &model.get_face(coedge.face)?.surface;
233 let seed = coedge.pcurve.evaluate(coedge.pcurve.mid_parameter()?)?;
234 let (u, v) = surface.project(point, seed[0], seed[1], PROJECT_ITERATIONS)?;
235 surface.normal(u, v)
236 };
237 let (Ok(na), Ok(nb)) = (normal(*a), normal(*b)) else {
238 return Ok(LineKind::Edge);
239 };
240 if !na.prod_cross(&nb).could_be_equal(&Vector3::zero()) {
241 return Ok(LineKind::Edge);
242 }
243 Ok(if na.prod_dot(d).could_be_equal(Scalar::ZERO) {
244 LineKind::Silhouette
245 } else {
246 LineKind::Tangent
247 })
248}
249
250pub(crate) struct Occluder {
252 face: FaceId,
253 surface: NurbSurface3D,
254 pub(crate) bounds: [[f64; 2]; 3],
256}
257
258impl Occluder {
259 pub(crate) fn of(model: &Model, face: FaceId) -> GeopResult<Self> {
260 let surface = model.get_face(face)?.surface.clone();
261 let bounds = surface
262 .aabb()
263 .map(|x| [x.lower().to_f64(), x.upper().to_f64()]);
264 Ok(Occluder {
265 face,
266 surface,
267 bounds,
268 })
269 }
270
271 fn could_meet(&self, a: &Vector3, b: &Vector3) -> bool {
273 (0..3).all(|k| {
274 let (x, y) = (a[k], b[k]);
275 let lo = x.lower().min(y.lower()).to_f64();
276 let hi = x.upper().max(y.upper()).to_f64();
277 lo <= self.bounds[k][1] && self.bounds[k][0] <= hi
278 })
279 }
280}
281
282pub(crate) fn seen(
288 model: &Model,
289 occluders: &[Occluder],
290 point: Vector3,
291 toward_eye: &Vector3,
292 length: Scalar,
293) -> GeopResult<bool> {
294 let far = point.add(&toward_eye.prod_scalar(length));
295 let ray = NurbCurve3D::line(point, far)?;
296 let coarse = Scalar::from_f64(COARSE_SUBDIVISION);
297 let mut behind_an_edge = false;
298 for occluder in occluders.iter().filter(|o| o.could_meet(&point, &far)) {
299 let (face_id, surface) = (occluder.face, &occluder.surface);
300 let ctx =
301 |e: GeopError| e.with_context(format!("the ray from {point:?} against face {face_id}"));
302 let overlaps =
303 curve_surface_overlaps(&ray, surface, coarse, MAX_NODES, min_subdivision_size())
304 .with_context(&ctx)?;
305 if !overlaps.is_empty() {
306 continue;
313 }
314 let crossings = curve_surface_crossings(&ray, surface, MAX_NODES, min_subdivision_size())
320 .or_else(|error| {
321 curve_surface_crossings(&ray, surface, MAX_NODES, coarse).map_err(|e| {
322 e.with_context(format!(
323 "searched again to the sheet's resolution, after: {error}"
324 ))
325 })
326 })
327 .with_context(&ctx)?;
328 for (t, uv) in crossings {
329 let (t, uv) = if t.definitely_greater(Scalar::ZERO) {
330 (t, uv)
331 } else {
332 refine_crossing(&ray, surface, t, uv)
333 };
334 if !t.definitely_greater(Scalar::ZERO) {
335 continue;
336 }
337 match face_contains(
338 model,
339 face_id,
340 uv[0].midpoint(),
341 uv[1].midpoint(),
342 MAX_NODES,
343 min_subdivision_size(),
344 FACE_CONTAINS_SEED,
345 )
346 .with_context(&ctx)?
347 {
348 PointClassification::Inside => return Ok(false),
349 PointClassification::Outside => {}
350 PointClassification::OnCoedge | PointClassification::OnVertex => {
351 behind_an_edge = true
352 }
353 }
354 }
355 }
356 Ok(!behind_an_edge)
357}
358
359pub(crate) fn occluders_bounds<'o>(
362 occluders: impl IntoIterator<Item = &'o Occluder>,
363) -> ([f64; 3], [f64; 3]) {
364 let mut lo = [f64::INFINITY; 3];
365 let mut hi = [f64::NEG_INFINITY; 3];
366 for o in occluders {
367 for k in 0..3 {
368 lo[k] = lo[k].min(o.bounds[k][0]);
369 hi[k] = hi[k].max(o.bounds[k][1]);
370 }
371 }
372 (lo, hi)
373}
374
375pub(crate) fn ray_length<'o>(occluders: impl IntoIterator<Item = &'o Occluder>) -> Scalar {
378 let (lo, hi) = occluders_bounds(occluders);
379 Scalar::from_f64(2.0 * diagonal(lo, hi) + 1.0)
380}
381
382pub(crate) fn cut_points(mut boxes: Vec<Scalar>, (t0, t1): (Scalar, Scalar)) -> Vec<Scalar> {
386 boxes.sort_by(|a, b| a.lower().to_f64().total_cmp(&b.lower().to_f64()));
387 let mut merged: Vec<Scalar> = Vec::new();
388 for b in boxes {
389 match merged.last_mut() {
390 Some(last) if last.could_be_equal(b) => *last = last.union(b),
391 _ => merged.push(b),
392 }
393 }
394 merged
395 .into_iter()
396 .filter(|b| b.definitely_greater(t0) && b.definitely_less(t1))
397 .map(|b| b.midpoint())
398 .collect()
399}
400
401pub fn project_view(
404 model: &Model,
405 faces: &[FaceId],
406 frame: &ViewFrame,
407 options: &ViewOptions,
408) -> GeopResult<ProjectedView> {
409 let d = frame.direction.vector();
410 let mut sources: Vec<(Source, Vec<Scalar>)> = Vec::new();
412 let mut add = |curve3: NurbCurve3D,
413 kind: LineKind,
414 edge: Option<EdgeId>,
415 silhouette: Option<Silhouette>,
416 on: Vec<FaceId>,
417 cuts: Vec<Scalar>|
418 -> GeopResult<()> {
419 let curve = frame.project_curve(&curve3)?;
420 if is_point(&curve)? {
421 return Ok(());
422 }
423 let bounds = bounds(&curve);
424 sources.push((
425 Source {
426 curve3,
427 curve,
428 kind,
429 edge,
430 silhouette,
431 on,
432 origin: sources.len(),
433 bounds,
434 },
435 cuts,
436 ));
437 Ok(())
438 };
439 for (edge_id, coedges) in edge_coedges(model, faces) {
440 let ctx = |e: GeopError| e.with_context(format!("project_view: edge {edge_id}"));
441 let kind = edge_kind(model, edge_id, &coedges, &d).with_context(&ctx)?;
442 if kind == LineKind::Tangent && !options.tangent_edges {
443 continue;
444 }
445 let curve = model.get_edge(edge_id)?.curve.clone();
446 let mut cuts = Vec::new();
448 let mut on = Vec::new();
449 for &coedge_id in &coedges {
450 let face = model.get_coedge(coedge_id)?.face;
451 on.push(face);
452 cuts.extend(edge_crossings(model, face, coedge_id, &curve, &d).with_context(&ctx)?);
453 }
454 add(curve, kind, Some(edge_id), None, on, cuts).with_context(&ctx)?;
455 }
456 for &face_id in faces {
457 for silhouette in face_silhouettes(model, face_id, &d)? {
458 add(
459 silhouette.curve.clone(),
460 LineKind::Silhouette,
461 None,
462 Some(silhouette),
463 vec![face_id],
464 Vec::new(),
465 )?;
466 }
467 }
468
469 let axes = [
475 Vector2::from_array([Scalar::ONE, Scalar::ZERO]),
476 Vector2::from_array([Scalar::ZERO, Scalar::ONE]),
477 ];
478 let mut monotone = Vec::with_capacity(sources.len());
479 for (source, mut stationary) in sources {
480 for axis in &axes {
481 stationary.extend(
482 source
483 .curve
484 .stationary_parameters(axis, MAX_NODES, min_subdivision_size())
485 .map_err(|e| {
486 e.with_context(format!(
487 "project_view: the {:?} of edge {:?}",
488 source.kind, source.edge
489 ))
490 })?,
491 );
492 }
493 let domain = source.curve.domain();
494 let mut ends = vec![domain.0];
495 ends.extend(cut_points(stationary, domain));
496 ends.push(domain.1);
497 if ends.len() == 2 {
498 monotone.push(source);
499 continue;
500 }
501 for pair in ends.windows(2) {
502 let curve = source.curve.sub_curve(pair[0], pair[1])?;
503 if is_point(&curve)? {
504 continue;
505 }
506 monotone.push(Source {
507 curve3: source.curve3.sub_curve(pair[0], pair[1])?,
508 bounds: bounds(&curve),
509 curve,
510 kind: source.kind,
511 edge: source.edge,
512 silhouette: source.silhouette.clone(),
513 on: source.on.clone(),
514 origin: source.origin,
515 });
516 }
517 }
518 let sources = monotone;
519
520 let mut cuts: Vec<Vec<Scalar>> = vec![Vec::new(); sources.len()];
522 for i in 0..sources.len() {
523 for j in i + 1..sources.len() {
524 if !boxes_meet(&sources[i].bounds, &sources[j].bounds) {
525 continue;
526 }
527 let bounds_silhouette = |a: &Source, b: &Source| {
532 a.edge.is_some()
533 && b.kind == LineKind::Silhouette
534 && b.edge.is_none()
535 && b.on.iter().any(|f| a.on.contains(f))
536 };
537 if bounds_silhouette(&sources[i], &sources[j])
538 || bounds_silhouette(&sources[j], &sources[i])
539 {
540 continue;
541 }
542 let (a, b) = (&sources[i], &sources[j]);
543 let (a_cuts, b_cuts) = meetings(
544 (&a.curve, &a.curve3),
545 (&b.curve, &b.curve3),
546 min_subdivision_size(),
547 )
548 .map_err(|e| {
549 let describe = |s: &Source| match s.edge {
550 Some(edge) => format!("{:?} of edge {edge} (of faces {:?})", s.kind, s.on),
551 None => format!(
552 "{:?} of face {:?}",
553 s.kind,
554 s.silhouette.as_ref().map(|s| s.face)
555 ),
556 };
557 e.with_context(format!(
558 "project_view: where the {} and the {} cross on the paper, seen through \
559 {frame:?}: on the paper {:?} and {:?}",
560 describe(&sources[i]),
561 describe(&sources[j]),
562 sources[i].curve,
563 sources[j].curve,
564 ))
565 })?;
566 cuts[i].extend(a_cuts);
567 cuts[j].extend(b_cuts);
568 }
569 }
570
571 let toward_eye = frame.toward_eye();
573 let occluders = faces
574 .iter()
575 .map(|&f| Occluder::of(model, f))
576 .collect::<GeopResult<Vec<_>>>()?;
577 let length = ray_length(&occluders);
578 let mut pieces: Vec<(usize, Scalar, Scalar, Result<bool, GeopError>)> = Vec::new();
581 for (index, (source, cuts)) in sources.iter().zip(cuts).enumerate() {
582 let domain = source.curve.domain();
583 let mut ends = vec![domain.0];
584 ends.extend(cut_points(cuts, domain));
585 ends.push(domain.1);
586 for pair in ends.windows(2) {
587 let (a, b) = (pair[0], pair[1]);
588 let ctx = |e: GeopError| {
589 let face = source.silhouette.as_ref().map(|s| s.face);
590 let (p, q) = (source.curve3.evaluate(a), source.curve3.evaluate(b));
591 e.with_context(format!(
592 "project_view: the piece [{a:?}, {b:?}] of a line of kind {:?} (edge {:?}, \
593 face {face:?}), from {p:?} to {q:?} in space",
594 source.kind, source.edge
595 ))
596 };
597 let decide = |f: f64| -> GeopResult<bool> {
603 let t = Scalar::fraction(a, b, Scalar::from_f64(f));
604 let mut point = source.curve3.evaluate(t)?;
605 if let Some(silhouette) = &source.silhouette {
606 point = silhouette.exact_point(model, &d, &point)?;
607 }
608 seen(model, &occluders, point, &toward_eye, length)
609 };
610 let mut outcome = Err(GeopError::new("no point tried"));
611 for f in [0.5, 0.25, 0.75] {
612 outcome = decide(f).map_err(ctx);
613 if outcome.is_ok() {
614 break;
615 }
616 }
617 pieces.push((index, a, b, outcome));
618 }
619 }
620 let known: Vec<Option<bool>> = pieces.iter().map(|p| p.3.as_ref().ok().copied()).collect();
624 let origin = |k: usize| sources[pieces[k].0].origin;
625 let mut lines = Vec::new();
626 for k in 0..pieces.len() {
627 let visible = match known[k] {
628 Some(v) => v,
629 None => {
630 let along = |i: usize| (origin(i) == origin(k)).then_some(known[i]).flatten();
631 let neighbour = (1..pieces.len()).find_map(|step| {
632 let before = k.checked_sub(step).and_then(along);
633 before.or_else(|| (k + step < pieces.len()).then(|| along(k + step)).flatten())
634 });
635 match neighbour {
636 Some(v) => v,
637 None => {
638 let (_, _, _, outcome) = pieces.swap_remove(k);
639 return Err(outcome.unwrap_err());
640 }
641 }
642 }
643 };
644 let (index, a, b, _) = &pieces[k];
645 let source = &sources[*index];
646 if !visible && !options.hidden_lines {
647 continue;
648 }
649 lines.push(ViewLine {
650 curve: source.curve.sub_curve(*a, *b)?,
651 curve3: source.curve3.sub_curve(*a, *b)?,
652 kind: source.kind,
653 visible,
654 edge: source.edge,
655 body: 0,
656 source: *index,
657 range: (*a, *b),
658 });
659 }
660
661 Ok(ProjectedView {
662 frame: *frame,
663 lines: rejoin(&sources, drop_drawn(lines, |_, _| true)?)?,
664 })
665}
666
667fn search(
672 a: &NurbCurve2D,
673 b: &NurbCurve2D,
674 reach: Scalar,
675 handoff: Scalar,
676) -> GeopResult<(Vec<Scalar>, Vec<Scalar>)> {
677 let (overlaps, crossings) =
678 curve_curve_overlaps_and_crossings(a, b, reach, MAX_NODES, handoff)?;
679 let (mut on_a, mut on_b) = (Vec::new(), Vec::new());
680 for (s, t) in crossings {
681 on_a.push(s);
682 on_b.push(t);
683 }
684 for o in overlaps {
685 on_a.extend([o.start.t, o.end.t]);
686 on_b.extend([o.start.partner, o.end.partner]);
687 }
688 Ok((on_a, on_b))
689}
690
691pub(crate) fn meetings(
721 (a, a3): (&NurbCurve2D, &NurbCurve3D),
722 (b, b3): (&NurbCurve2D, &NurbCurve3D),
723 handoff: Scalar,
724) -> GeopResult<(Vec<Scalar>, Vec<Scalar>)> {
725 let coarse = Scalar::from_f64(COARSE_SUBDIVISION);
726 let fine = || -> GeopResult<(Vec<Scalar>, Vec<Scalar>)> {
727 let error = match search(a, b, coarse, handoff) {
728 Ok(found) => return Ok(found),
729 Err(e) => e,
730 };
731 let ends = |c: &NurbCurve3D| -> GeopResult<[Vector3; 2]> {
732 let (t0, t1) = c.domain();
733 Ok([c.evaluate(t0)?, c.evaluate(t1)?])
734 };
735 let (ea, eb) = (ends(a3)?, ends(b3)?);
736 let shared = (0..2)
737 .flat_map(|i| (0..2).map(move |j| (i, j)))
738 .find(|&(i, j)| ea[i].could_be_equal(&eb[j]));
739 let ends_context = format!("their ends in space: {ea:?} and {eb:?}");
740 let Some((end_a, end_b)) = shared else {
741 return Err(error.with_context(ends_context));
742 };
743 let error = error.with_context(ends_context);
744 let halves = |c: &NurbCurve2D| -> GeopResult<[NurbCurve2D; 2]> {
745 let (l, r) = c.split_mid()?;
746 Ok([l, r])
747 };
748 let (ha, hb) = (halves(a)?, halves(b)?);
749 let (mut on_a, mut on_b) = (Vec::new(), Vec::new());
750 for (i, half_a) in ha.iter().enumerate() {
751 for (j, half_b) in hb.iter().enumerate() {
752 if i == end_a && j == end_b {
753 continue;
754 }
755 let (x, y) = search(half_a, half_b, coarse, handoff).map_err(|e| {
756 e.with_context(format!(
757 "searched again without the common end, after: {error}"
758 ))
759 })?;
760 on_a.extend(x);
761 on_b.extend(y);
762 }
763 }
764 Ok((on_a, on_b))
765 };
766 fine().or_else(|error| {
767 search(a, b, coarse, coarse).map_err(|e| {
768 e.with_context(format!(
769 "searched again to the sheet's resolution, after: {error}"
770 ))
771 })
772 })
773}
774
775fn rejoin(sources: &[Source], mut lines: Vec<ViewLine>) -> GeopResult<Vec<ViewLine>> {
779 lines.sort_by(|a, b| {
780 (a.source, a.range.0.to_f64())
781 .partial_cmp(&(b.source, b.range.0.to_f64()))
782 .unwrap_or(std::cmp::Ordering::Equal)
783 });
784 let mut joined: Vec<ViewLine> = Vec::new();
785 for line in lines {
786 if let Some(last) = joined.last_mut()
787 && last.source == line.source
788 && last.visible == line.visible
789 && last.range.1.could_be_equal(line.range.0)
790 {
791 last.range.1 = line.range.1;
792 continue;
793 }
794 joined.push(line);
795 }
796 let mut out: Vec<ViewLine> = Vec::new();
798 for mut line in joined {
799 let source = &sources[line.source];
800 let (t0, t1) = source.curve.domain();
801 let whole = line.range.0.could_be_equal(t0) && line.range.1.could_be_equal(t1);
802 if whole {
803 line.curve = source.curve.clone();
804 line.curve3 = source.curve3.clone();
805 } else {
806 line.curve = source.curve.sub_curve(line.range.0, line.range.1)?;
807 line.curve3 = source.curve3.sub_curve(line.range.0, line.range.1)?;
808 }
809 out.push(line);
810 }
811 Ok(out)
812}
813
814pub(crate) fn drop_drawn(
820 mut lines: Vec<ViewLine>,
821 compare: impl Fn(&ViewLine, &ViewLine) -> bool,
822) -> GeopResult<Vec<ViewLine>> {
823 lines.sort_by_key(|l| !l.visible);
824 let mut kept: Vec<(ViewLine, [f64; 4])> = Vec::new();
825 let reach = Scalar::from_f64(COARSE_SUBDIVISION);
828 let around = reach.neg().union(reach);
829 for line in lines {
830 let mid = (line.curve.evaluate(line.curve.mid_parameter()?)?).map(|x| x.add(around));
831 let here = bounds(&line.curve);
832 let mut drawn = false;
833 for (other, there) in &kept {
834 if boxes_meet(&here, there)
835 && compare(&line, other)
836 && curve_could_contain(&other.curve, &mid, MAX_NODES, min_subdivision_size())?
837 .is_some()
838 {
839 drawn = true;
840 break;
841 }
842 }
843 if !drawn {
844 kept.push((line, here));
845 }
846 }
847 Ok(kept.into_iter().map(|(l, _)| l).collect())
848}