Skip to main content

geop_ops_drawing/
section.rs

1//! Section views: the part cut by a plane, with what lies on the side its
2//! normal points to taken away — by the kernel's own boolean, subtracting a
3//! box standing on the plane from every solid.
4
5use geop_core_geometry::aabb::union_aabb;
6use geop_core_math::{
7    geop_error::{GeopError, GeopResult, WithContext},
8    plain,
9    scalars::Scalar,
10    vector::{Vector2, Vector3},
11};
12use geop_core_topology::{FaceId, Model, SolidId};
13use geop_lib_booleans::{RemeshParams, boolean};
14use geop_lib_sweep::{SweepLoop, extrude, polygon};
15use geop_ops_model::{BooleanOp, Profile};
16use geop_ops_model::{NamedModel, Namer, frame_along};
17
18/// The box holding every face of `model`, `[lo, hi]` per axis.
19fn bounds(model: &Model) -> GeopResult<[[f64; 2]; 3]> {
20    let b = union_aabb(model.faces.values().map(|f| f.surface.aabb()))
21        .ok_or_else(|| GeopError::new("the part has no face"))?;
22    Ok(b.map(|x| [x.lower().to_f64(), x.upper().to_f64()]))
23}
24
25/// `model` with every solid cut by the plane through `origin` normal to the
26/// unit vector `normal`, the side `normal` points to taken away. A solid
27/// wholly on that side is gone; faces standing on their own are left whole.
28pub fn section_part(
29    model: &NamedModel,
30    origin: &Vector3,
31    normal: &Vector3,
32) -> GeopResult<NamedModel> {
33    let ctx = |e: GeopError| {
34        e.with_context(format!(
35            "section_part(origin={origin:?}, normal={normal:?})"
36        ))
37    };
38    let mut cut = model.clone();
39    // Ordered by id, as the model's arenas are.
40    let solids: Vec<SolidId> = cut.topology().solids.keys().copied().collect();
41    if solids.is_empty() {
42        return Err(GeopError::new("the part has no solid to cut")).with_context(&ctx);
43    }
44    // A box reaching past the whole part from wherever on the plane.
45    let b = bounds(cut.topology()).with_context(&ctx)?;
46    let center: Vec<f64> = b.iter().map(|[lo, hi]| (lo + hi) / 2.0).collect();
47    let diagonal = plain::diagonal(b.map(|[lo, _]| lo), b.map(|[_, hi]| hi));
48    let offset: f64 = (0..3)
49        .map(|k| (origin[k].to_f64() - center[k]).powi(2))
50        .sum::<f64>()
51        .sqrt();
52    let reach = Scalar::from_f64(2.0 * (diagonal + offset) + 1.0);
53    let plane = frame_along(*origin, normal).with_context(&ctx)?;
54    let square = [
55        Vector2::from_array([reach.neg(), reach.neg()]),
56        Vector2::from_array([reach, reach.neg()]),
57        Vector2::from_array([reach, reach]),
58        Vector2::from_array([reach.neg(), reach]),
59    ];
60    for (k, solid) in solids.into_iter().enumerate() {
61        let tool_namer = Namer::new("section", &format!("tool{k}"))?;
62        let built = extrude(
63            &mut cut,
64            &tool_namer,
65            Some(&tool_namer.root()),
66            &plane,
67            &[Scalar::ZERO, reach],
68            &[SweepLoop::plain(Profile::closed(
69                polygon(&square).with_context(&ctx)?,
70            ))],
71        )
72        .with_context(&ctx)?;
73        let tool = built
74            .solid
75            .ok_or_else(|| GeopError::new("the cutting box is no solid"))
76            .with_context(&ctx)?;
77        let namer = Namer::new("section", &format!("cut{k}"))?;
78        boolean(
79            &mut cut,
80            &namer,
81            solid,
82            tool,
83            BooleanOp::Difference,
84            RemeshParams::default(),
85        )
86        .map_err(|e| ctx(e.with_context(format!("cutting solid {solid}"))))?;
87    }
88    Ok(cut)
89}
90
91/// The faces of `faces` lying in the plane through `origin` normal to
92/// `normal`: a section's cut.
93pub fn cut_faces(
94    model: &Model,
95    faces: &[FaceId],
96    origin: &Vector3,
97    normal: &Vector3,
98) -> GeopResult<Vec<FaceId>> {
99    let mut cut = Vec::new();
100    for &face in faces {
101        let Some(plane) = model.get_face(face)?.surface.as_plane()? else {
102            continue;
103        };
104        if plane
105            .normal
106            .prod_cross(normal)
107            .could_be_equal(&Vector3::zero())
108            && plane
109                .point
110                .sub(origin)
111                .prod_dot(normal)
112                .could_be_equal(Scalar::ZERO)
113        {
114            cut.push(face);
115        }
116    }
117    Ok(cut)
118}