Skip to main content

geop_ops_model/spacing/
holes.rs

1//! Where a part's holes are and what fits them: each hole a step drilled
2//! recorded as a [`HoleSite`] — its axis, the screw size it is for, how a
3//! head seats in it and how deep it runs — in the part's target
4//! [`hole_sites`], which every step that drills holes revises, and every
5//! step that repeats features copies with them. What reads them: a pattern
6//! laid out at holes ([`super::Frames::Holes`]), and fasteners placed in
7//! them, sized by them.
8
9use std::collections::BTreeMap;
10
11use geop_core_geometry::shape::Axis;
12use geop_core_math::{
13    geop_error::{GeopError, GeopResult, WithContext},
14    primitives::{CoordinateSystem, Motion, Pose, Quaternion},
15    scalars::Scalar,
16    with_context,
17};
18use geop_core_program::{
19    element::{ElementReference, HasElements, Kind, whole},
20    reader::Reader,
21    target::{Target, TargetReference},
22};
23use serde::{Deserialize, Serialize};
24
25use crate::{FeatureRef, frame_along, operation_of, parameters::located};
26
27/// How a hole takes a screw.
28#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum HoleFit {
31    /// The screw passes through: a clearance hole.
32    #[default]
33    Clearance,
34    /// The screw passes through, its head sunk in a counterbore.
35    Counterbore,
36    /// The screw passes through, its head sunk flush in a countersink.
37    Countersink,
38    /// The screw is screwed in: a tapped hole.
39    Tapped,
40}
41
42/// One hole, as the step that drilled it recorded it — kept as geometry,
43/// so that it says the same whatever later steps do to its faces.
44#[derive(Clone, Debug, PartialEq)]
45pub struct HoleSite {
46    /// Where it enters its face, and the way it runs into the material
47    /// from there (a unit vector).
48    pub axis: Axis,
49    /// The ISO metric size of the screw it is for: `M6`. None for a hole
50    /// sized by hand, or tapped for a pipe thread.
51    pub size: Option<String>,
52    pub fit: HoleFit,
53    /// How far below its face a screw's head seats: a counterbore's depth;
54    /// none for any other.
55    pub seat: f64,
56    /// How far it runs from its face: a blind hole's depth, or, through
57    /// the material, as far as its wall goes.
58    pub depth: f64,
59    /// It comes out of the material at its far end.
60    pub through: bool,
61}
62
63impl HoleSite {
64    /// The frame a screw's head seats on: on the axis, as far below the
65    /// face as a head seats, its `w` pointing out of the hole — the way a
66    /// standard screw's datum `seat` faces, its shank running down `-w`
67    /// into the hole.
68    pub fn seat_frame(&self) -> GeopResult<CoordinateSystem> {
69        let at = self.along(self.seat);
70        frame_along(at, &self.axis.direction.neg())
71    }
72
73    /// The frame where it comes out of the material, its `w` pointing out
74    /// of the hole there — where a nut on a screw through it seats. None for
75    /// a blind hole.
76    pub fn exit_frame(&self) -> GeopResult<Option<CoordinateSystem>> {
77        if !self.through {
78            return Ok(None);
79        }
80        Ok(Some(frame_along(
81            self.along(self.depth),
82            &self.axis.direction,
83        )?))
84    }
85
86    /// How long the hole is below where a head seats: the grip of a screw
87    /// through it, or how far a screw can go into it.
88    pub fn grip(&self) -> f64 {
89        self.depth - self.seat
90    }
91
92    /// The point `distance` along its axis from where it enters its face.
93    fn along(&self, distance: f64) -> geop_core_math::vector::Vector3 {
94        (self.axis.point).add(&self.axis.direction.prod_scalar(Scalar::from_f64(distance)))
95    }
96
97    /// The same hole, moved by `motion`.
98    pub fn moved(&self, motion: &Motion) -> Self {
99        Self {
100            axis: Axis {
101                point: motion.apply(&self.axis.point),
102                direction: motion.rotate(&self.axis.direction),
103            },
104            ..self.clone()
105        }
106    }
107}
108
109/// The pose of the frame `frame`: what takes the world's origin and axes
110/// to it.
111pub fn pose_of(frame: &CoordinateSystem) -> GeopResult<Pose> {
112    let rotation = Quaternion::from_rotation_columns([*frame.u(), *frame.v(), *frame.w()])?;
113    Pose::new(*frame.origin(), rotation)
114}
115
116/// A part's holes, by name: the value of its target [`hole_sites`].
117#[derive(Clone, Debug, Default, PartialEq)]
118pub struct HoleSites(BTreeMap<String, HoleSite>);
119
120/// The part's holes.
121pub fn hole_sites() -> TargetReference<HoleSites> {
122    TargetReference::new("holes")
123}
124
125/// What a part reads as its holes before a step drilled one: none.
126static NO_HOLES: HoleSites = HoleSites(BTreeMap::new());
127
128impl HoleSites {
129    /// Records `site` under `name`. Fails, leaving the holes as they are,
130    /// if there is a hole of that name already.
131    pub fn add(&mut self, name: impl Into<String>, site: HoleSite) -> GeopResult<()> {
132        let name = name.into();
133        if self.0.contains_key(&name) {
134            return Err(GeopError::new(format!(
135                "the part has a hole named {name:?} already"
136            )));
137        }
138        self.0.insert(name, site);
139        Ok(())
140    }
141
142    /// Every hole, by name.
143    pub fn iter(&self) -> impl Iterator<Item = (&str, &HoleSite)> {
144        self.0.iter().map(|(name, site)| (name.as_str(), site))
145    }
146
147    /// The holes the step `step` drilled — those named after it — by name,
148    /// in order.
149    pub fn of_step(&self, step: &str) -> Vec<(&str, &HoleSite)> {
150        (self.iter())
151            .filter(|(name, _)| operation_of(name) == Some(step))
152            .collect()
153    }
154}
155
156/// Every step that drills holes, or repeats them, adds to them.
157impl Target for HoleSites {
158    const CHAIN: bool = true;
159}
160
161/// Read whole.
162impl Kind for HoleSites {
163    const NAME: &'static str = "holes";
164}
165
166impl HasElements<HoleSites> for HoleSites {
167    type Element<'a> = &'a HoleSites;
168    const SINGLETON: Option<&'static str> = Some("holes");
169
170    fn element(&self, name: &str) -> GeopResult<&HoleSites> {
171        whole(self, name)
172    }
173
174    fn undefined() -> Option<&'static Self> {
175        Some(&NO_HOLES)
176    }
177}
178
179/// The part's holes as `reader` reads them: none, before the first step
180/// that drills one.
181pub fn read_hole_sites<'a>(reader: &Reader<'a>) -> GeopResult<&'a HoleSites> {
182    reader.retrieve_element(&ElementReference::whole(hole_sites()))
183}
184
185/// The holes of the features `features` — of the part `reader` reads, or
186/// of parts placed in it — each by its name, behind its instance's path,
187/// and where the part read has it. A feature that drilled no hole is
188/// refused by name.
189pub fn holes_of(reader: &Reader, features: &[FeatureRef]) -> GeopResult<Vec<(String, HoleSite)>> {
190    let mut found = Vec::new();
191    for feature in features {
192        let ctx = with_context!("the holes of {feature:?}");
193        let instance = feature.instance();
194        let all = ElementReference::<HoleSites, HoleSites>::whole(hole_sites());
195        let all = match instance.is_empty() {
196            true => all,
197            false => all.behind(instance),
198        };
199        let sites = reader.retrieve_placed(&all).with_context(ctx)?;
200        let motion = located(reader, instance).with_context(ctx)?.motion();
201        let of = sites.element.of_step(feature.name());
202        if of.is_empty() {
203            return Err(GeopError::new(format!(
204                "the feature {:?} drilled no holes: pick the wall of a hole a hole step drilled, or a pattern of one",
205                feature.name()
206            )))
207            .with_context(ctx);
208        }
209        for (name, site) in of {
210            let name = match instance.is_empty() {
211                true => name.to_string(),
212                false => geop_core_program::placed::Located::in_instance(instance, name).key(),
213            };
214            found.push((name, site.moved(&motion)));
215        }
216    }
217    Ok(found)
218}