Skip to main content

geop_ops_model/
threads.rs

1//! Cosmetic threads: threads recorded on a round face rather than modelled
2//! — what a drawing, a bill of materials or a fastener check reads, and
3//! what the viewer draws as a helix on the face. Held as the part's target
4//! [`threads`], and drawn as the [`annotations`] of the step that recorded
5//! them ([`draw_threads`]).
6//!
7//! They are the framework's, not the hole's that taps one: a step that
8//! repeats features — a pattern, a mirror — copies the threads they
9//! carry and draws its copies, knowing no step that records one.
10
11use std::collections::BTreeMap;
12
13use crate::{frame_along, operation_of};
14use geop_core_editor::view::{Annotations, ViewAnnotation, annotations};
15use geop_core_geometry::{
16    nurb_curve::{Handedness, Helix, NurbCurve3D},
17    shape::Axis,
18};
19use geop_core_math::{
20    geop_error::{GeopError, GeopResult},
21    primitives::CoordinateSystem,
22    scalars::Scalar,
23    vector::Vector3,
24};
25use geop_core_program::{
26    element::{ElementReference, HasElements, Kind, whole},
27    part::StepContext,
28    reader::Reader,
29    target::{Target, TargetReference},
30};
31
32/// A thread recorded on a round face, straight or tapered, by its
33/// specification and where
34/// it lies — kept as geometry, so that it says the same whatever later
35/// steps do to the face it was put on.
36#[derive(Clone, Debug, PartialEq)]
37pub struct CosmeticThread {
38    /// How it is called: `M6x1`.
39    pub designation: String,
40    /// The face it was put on, by its name then.
41    pub face: String,
42    /// Where it starts, on the axis of its face, and the way it runs from
43    /// there (a unit vector).
44    pub axis: Axis,
45    /// The radius of the face it lies on where it starts: where it is
46    /// drawn.
47    pub radius: Scalar,
48    /// How much that radius, and the thread's diameters, change per unit
49    /// along the axis from where it starts: none for a straight thread, and
50    /// for a pipe thread (NPT) a thirty-second, shrinking into a hole from
51    /// its face, growing along a pipe from its end.
52    pub taper: Scalar,
53    /// The thread's nominal (major) diameter, its minor diameter where it
54    /// starts, and its pitch, as its standard gives them.
55    pub major_diameter: f64,
56    pub minor_diameter: f64,
57    pub pitch: f64,
58    /// How far along the axis it runs from where it starts.
59    pub length: f64,
60    /// In a hole (a nut's thread) rather than on a shaft (a bolt's).
61    pub internal: bool,
62    pub handedness: Handedness,
63}
64
65impl CosmeticThread {
66    /// The helix it is drawn as: on its face, from where it starts, one
67    /// turn per pitch, as far as it runs, following the face's taper.
68    pub fn helix(&self) -> GeopResult<NurbCurve3D> {
69        let frame: CoordinateSystem = frame_along(self.axis.point, &self.axis.direction)?;
70        let helix = Helix::new(
71            Scalar::from_f64(self.pitch),
72            self.length / self.pitch,
73            self.handedness,
74        );
75        NurbCurve3D::helix(&frame, self.radius, &helix.tapered(self.taper))
76    }
77
78    /// The helix as a polyline, `SEGMENTS_PER_SPAN` segments to a quarter turn.
79    pub fn polyline(&self) -> GeopResult<Vec<Vector3>> {
80        let helix = self.helix()?;
81        // A span between every pair of the double interior knots.
82        let spans = (helix.control_points.len() - 1) / 2;
83        let n = (spans * SEGMENTS_PER_SPAN) as i64;
84        (0..=n)
85            .map(|i| helix.evaluate(Scalar::from_ratio(i, n)?))
86            .collect()
87    }
88}
89
90/// Segments a cosmetic thread's helix is drawn with per span of a quarter
91/// turn.
92const SEGMENTS_PER_SPAN: usize = 8;
93
94/// A part's cosmetic threads, by name: the value of its target [`threads`],
95/// which every step that records one revises.
96#[derive(Clone, Debug, Default, PartialEq)]
97pub struct Threads(BTreeMap<String, CosmeticThread>);
98
99/// The part's cosmetic threads.
100pub fn threads() -> TargetReference<Threads> {
101    TargetReference::new("threads")
102}
103
104impl Threads {
105    /// Records `thread` under `name`. Fails, leaving the threads as they
106    /// are, if there is a thread of that name already.
107    pub fn add(&mut self, name: impl Into<String>, thread: CosmeticThread) -> GeopResult<()> {
108        let name = name.into();
109        if self.0.contains_key(&name) {
110            return Err(GeopError::new(format!(
111                "the part has a thread named {name:?} already"
112            )));
113        }
114        self.0.insert(name, thread);
115        Ok(())
116    }
117
118    /// Every cosmetic thread, by name.
119    pub fn iter(&self) -> impl Iterator<Item = (&str, &CosmeticThread)> {
120        self.0.iter().map(|(name, thread)| (name.as_str(), thread))
121    }
122
123    /// The cosmetic thread `name`.
124    pub fn get(&self, name: &str) -> GeopResult<&CosmeticThread> {
125        (self.0.get(name))
126            .ok_or_else(|| GeopError::new(format!("the part has no thread named {name:?}")))
127    }
128
129    /// The cosmetic threads the step `step` recorded — those named after
130    /// it — by name.
131    pub fn of_step(&self, step: &str) -> Vec<String> {
132        (self.0.keys())
133            .filter(|name| operation_of(name) == Some(step))
134            .cloned()
135            .collect()
136    }
137}
138
139impl Threads {
140    /// What a viewer draws of each thread the step `step` recorded: its
141    /// name, its designation and its helix as a polyline.
142    pub fn drawn(&self, step: &str) -> GeopResult<Vec<ViewAnnotation>> {
143        (self.0.iter())
144            .filter(|(name, _)| operation_of(name) == Some(step))
145            .map(|(name, thread)| {
146                Ok(ViewAnnotation {
147                    name: name.clone(),
148                    label: thread.designation.clone(),
149                    polyline: thread.polyline()?,
150                })
151            })
152            .collect()
153    }
154}
155
156/// Every step that records threads adds to them.
157impl Target for Threads {
158    const CHAIN: bool = true;
159}
160
161/// Read whole.
162impl Kind for Threads {
163    const NAME: &'static str = "threads";
164}
165
166impl HasElements<Threads> for Threads {
167    type Element<'a> = &'a Threads;
168    const SINGLETON: Option<&'static str> = Some("threads");
169
170    fn element(&self, name: &str) -> GeopResult<&Threads> {
171        whole(self, name)
172    }
173}
174
175/// The part's cosmetic threads as `reader` reads them: none, before the
176/// first step that records one.
177pub fn read_threads<'a>(reader: &Reader<'a>) -> GeopResult<std::borrow::Cow<'a, Threads>> {
178    if !reader.defines(&threads()) {
179        return Ok(std::borrow::Cow::Owned(Threads::default()));
180    }
181    let whole = ElementReference::<Threads, Threads>::whole(threads());
182    Ok(std::borrow::Cow::Borrowed(reader.retrieve_element(&whole)?))
183}
184
185/// Draws the threads the step recorded on the part, as its
186/// [`annotations`]: what a step that records threads does after.
187pub fn draw_threads(ctx: &mut StepContext) -> GeopResult<()> {
188    let step = ctx.step();
189    ctx.produce(&annotations(step), &step.to_string(), draw)
190}
191
192/// The make of [`draw_threads`]: its arguments, the step, are what a
193/// node is made from, so owned.
194#[allow(clippy::ptr_arg)]
195fn draw(reader: &Reader, step: &String) -> GeopResult<Annotations> {
196    Ok(Annotations(read_threads(reader)?.drawn(step)?))
197}