Skip to main content

geop_ops_model/
extent.rs

1//! How far a tool goes into the solid it is combined with ([`Extent`]): a
2//! length, up to the next face, or through all — and what sizes a tool to
3//! reach past that solid ([`hull`], [`reach_past`]). An extrude, a revolve
4//! and a hole say how far they go this way, and [`crate::Combine`] then
5//! stops a tool going up to the next face where [`crate::Tool`] says.
6
7use crate::NamedModel;
8use geop_core_editor::editing::Choice;
9use geop_core_geometry::aabb::union_aabb;
10use geop_core_math::{
11    geop_error::{GeopError, GeopResult},
12    plain,
13    primitives::CoordinateSystem,
14    vector::Vector3,
15};
16use geop_core_program::{numeric::NumericInput, reader::Reader};
17use geop_core_topology::{Body, FaceId, Model, boundary::BoundaryType};
18use serde::{Deserialize, Serialize};
19
20/// How far one side of a tool goes: a length — an angle, in degrees, for a
21/// revolve — plain or a formula of the part's parameters, or as far as the
22/// solid it is combined with: up to its next face, or through all of it.
23/// Serialized as `{"blind": 20.0}`, `{"blind": {"formula": "height - 2"}}`,
24/// `"up_to_next"` or `"through_all"`.
25#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27pub enum Extent {
28    Blind(NumericInput),
29    UpToNext,
30    ThroughAll,
31}
32
33/// Where a side ends: an [`Extent`], its length evaluated.
34#[derive(Clone, Copy, Debug, PartialEq)]
35pub enum End {
36    Blind(f64),
37    UpToNext,
38    ThroughAll,
39}
40
41impl End {
42    /// Its length, if it is blind.
43    pub fn length(self) -> Option<f64> {
44        match self {
45            End::Blind(length) => Some(length),
46            End::UpToNext | End::ThroughAll => None,
47        }
48    }
49}
50
51impl Extent {
52    /// A side `amount` far: a number, or a formula's text.
53    pub fn blind(amount: impl Into<NumericInput>) -> Self {
54        Extent::Blind(amount.into())
55    }
56
57    /// Its length, to change, if it is blind.
58    pub fn length_mut(&mut self) -> Option<&mut NumericInput> {
59        match self {
60            Extent::Blind(length) => Some(length),
61            Extent::UpToNext | Extent::ThroughAll => None,
62        }
63    }
64
65    /// Where it ends, its length as `reader` reads the parameters.
66    pub fn end(&self, reader: &Reader) -> GeopResult<End> {
67        Ok(match self {
68            Extent::Blind(length) => End::Blind(length.resolve(reader)?),
69            Extent::UpToNext => End::UpToNext,
70            Extent::ThroughAll => End::ThroughAll,
71        })
72    }
73
74    /// The ways a side can end, as a dialog offers them — with `none` for a
75    /// side which may not be.
76    pub fn choices(none: bool) -> Vec<Choice> {
77        let mut choices = Vec::new();
78        if none {
79            choices.push(Choice::new("none", "None"));
80        }
81        choices.push(Choice::new("blind", "Blind"));
82        choices.push(Choice::new("up_to_next", "Up to next"));
83        choices.push(Choice::new("through_all", "Through all"));
84        choices
85    }
86
87    /// How `extent` ends, as [`Extent::choices`] offers it.
88    pub fn mode(extent: Option<&Extent>) -> &'static str {
89        match extent {
90            None => "none",
91            Some(Extent::Blind(_)) => "blind",
92            Some(Extent::UpToNext) => "up_to_next",
93            Some(Extent::ThroughAll) => "through_all",
94        }
95    }
96
97    /// The extent `mode` chooses, keeping the length of `old` if it was
98    /// blind, else `default` far.
99    pub fn pick(old: Option<&Extent>, mode: &str, default: f64) -> Option<Extent> {
100        match mode {
101            "blind" => Some(Extent::Blind(match old {
102                Some(Extent::Blind(d)) => d.clone(),
103                _ => NumericInput::Plain(default),
104            })),
105            "up_to_next" => Some(Extent::UpToNext),
106            "through_all" => Some(Extent::ThroughAll),
107            _ => None,
108        }
109    }
110}
111
112/// Points whose convex hull holds the solids named `targets` (see
113/// [`face_hull`]). None for no solids.
114pub fn hull(model: &NamedModel, targets: &[String]) -> GeopResult<Option<Vec<Vector3>>> {
115    let topology = model.topology();
116    let mut faces = Vec::new();
117    for target in targets {
118        faces.extend(topology.body_faces(Body::Solid(model.solid_id(target)?))?);
119    }
120    let points = face_hull(topology, &faces)?;
121    Ok((!points.is_empty()).then_some(points))
122}
123
124/// Points whose convex hull holds `faces`: the control points of their
125/// surfaces, each restricted to the box in `(u, v)` around its face's outer
126/// loop — a NURBS surface lies within the hull of its control points, and
127/// a face within the box around its outer loop — enclosed, for sizing a
128/// tool that has to reach past them, and for telling which side of a line
129/// they lie on, which for a solid touching the line is decided by their
130/// enclosures, not by their midpoints.
131///
132/// The restriction matters where a face is a small part of its surface: a
133/// shell's floor is a disc of radius 36 on a patch of 240 square, and the
134/// patch's corners stood far out to every side of an axis the cup only
135/// touched (see `mug_handle_turned_up_to_next_around_the_cup_seam` in
136/// `geop-ops-extrude-revolve`).
137pub fn face_hull(model: &Model, faces: &[FaceId]) -> GeopResult<Vec<Vector3>> {
138    let mut points = Vec::new();
139    for &face in faces {
140        let face = model.get_face(face)?;
141        let outer = match face.outer {
142            BoundaryType::Loop(anchor) => model
143                .iterate_loop_coedges(anchor)
144                .map(|c| Ok(model.get_coedge(c)?.pcurve.aabb()))
145                .collect::<GeopResult<Vec<_>>>()?,
146            BoundaryType::Vertex(_) => Vec::new(),
147        };
148        let surface = match union_aabb(outer) {
149            Some(uv) => face.surface.sub_surface(
150                (uv[0].lower(), uv[0].upper()),
151                (uv[1].lower(), uv[1].upper()),
152            )?,
153            None => face.surface.clone(),
154        };
155        for cp in &surface.control_points {
156            points.push(cp.dehomogenized()?);
157        }
158    }
159    Ok(points)
160}
161
162/// How far along `plane`'s normal the nearest and the furthest of
163/// `points` lie from it.
164pub fn spread_along(points: &[Vector3], plane: &CoordinateSystem) -> (f64, f64) {
165    let o = plane.origin().to_f64_array();
166    let w = plane.w().to_f64_array();
167    let along = |p: &Vector3| {
168        let p = p.to_f64_array();
169        (0..3).map(|k| (p[k] - o[k]) * w[k]).sum::<f64>()
170    };
171    points
172        .iter()
173        .map(along)
174        .fold((f64::INFINITY, f64::NEG_INFINITY), |(near, far), x| {
175            (near.min(x), far.max(x))
176        })
177}
178
179/// How far along `plane`'s normal — forwards for `sign = 1`, backwards for
180/// `-1` — reaches past everything of the solid whose convex hull `hull`
181/// spans: a side going up to its next face has to be built at least that
182/// long to meet whichever face that is.
183pub fn reach_past(hull: &[Vector3], plane: &CoordinateSystem, sign: f64) -> GeopResult<f64> {
184    let (near, far) = spread_along(hull, plane);
185    let ahead = if sign > 0.0 { far } else { -near };
186    if ahead <= 0.0 {
187        return Err(GeopError::new(format!(
188            "up to next: nothing of the target lies {} the sketch, so there is no next face",
189            if sign > 0.0 { "in front of" } else { "behind" }
190        )));
191    }
192    // Well past the target, so the tool's end is clear of all of it.
193    let (lo, hi) = plain::bounds(hull.iter().map(Vector3::to_f64_array))
194        .ok_or_else(|| GeopError::new("up to next: the target has no faces"))?;
195    Ok(ahead + plain::diagonal(lo, hi))
196}