Skip to main content

geop_lib_stdlib/
lib.rs

1//! The standard parts library: fasteners and purchased components every
2//! workspace can place, as read-only files named `std:…` (see
3//! [`geop_core_program::workspace::is_shared`]) — `std:iso4032_hex_nut.geop`.
4//!
5//! Each family is one program, generated here in Rust and never saved:
6//! its sizes are the rows of its table parameter `size` — `M5`,
7//! `M3x10` — its dimensions read from that row by the formulas of its
8//! sketches (see [`tables`] for the data, [`drawing`] for sketches drawn
9//! by formulas, [`steps`] for how a part is built). So a program placing
10//! one picks its size like any parameter of a placed part.
11//!
12//! Every part turns around, or runs along, the `z` axis, and carries named
13//! datums to mate it by: `axis`, and a plane through the origin for the
14//! face it sits on — `seat` under a screw's head, `top` of a countersunk
15//! screw, `base` of a nut, washer, pin or standoff, `side` of a bearing or
16//! a gear, `end` of an extrusion, pulley or coupling, `face` of a motor,
17//! `mount` of a servo; a part running along `z` sits on a plane beside it,
18//! `back` of a rack, `base` of a rail, `top` of a carriage. A concentric
19//! mate on `axis` and a coincident one on that plane place it. The `axis`
20//! is a joint's connector too: a gear or a pulley turns about it, a
21//! carriage slides along its rail's, a servo's horn turns about it — and a
22//! gear coupling ties two gears, or two pulleys on a belt (see [`drive`]).
23//!
24//! Threads are not modelled: a screw's shank and a nut's bore are their
25//! nominal diameter, and [`StandardPart::threaded`] names the faces a
26//! thread goes on, for it to be recorded on them.
27//!
28//! Where the files come from is [`WithStandardParts`]: a workspace's own
29//! files with the standard parts beside them, which is what the editor's
30//! workspace reads.
31//!
32//! A part of a program of its own drawn by formulas, as the families are,
33//! is a [`Drawing`] on the [`outline_plane`].
34
35// Library code reports what goes wrong as an error, never by panicking;
36// tests may unwrap.
37#![cfg_attr(
38    not(test),
39    deny(
40        clippy::unwrap_used,
41        clippy::expect_used,
42        clippy::panic,
43        clippy::todo,
44        clippy::unimplemented,
45        clippy::unreachable
46    )
47)]
48
49mod components;
50mod drawing;
51mod drive;
52mod fasteners;
53mod involute;
54mod steps;
55mod tables;
56
57pub use drawing::Drawing;
58pub use drive::TEETH;
59pub use steps::{SIZE, outline_plane};
60
61use geop_core_program::{
62    program::Program,
63    workspace::{FileStore, FileStoreMut},
64};
65use geop_ops_model::parameters::{parameters_of, set_parameters};
66use std::{
67    ops::{Deref, DerefMut},
68    sync::OnceLock,
69};
70
71use geop_core_math::geop_error::{GeopError, GeopResult};
72
73/// What the standard parts' file names start with.
74pub const PREFIX: &str = "std:";
75
76/// A family of standard parts: the file it is placed from, and the
77/// program that builds its every size.
78pub struct StandardPart {
79    /// Its file name, [`PREFIX`] first.
80    pub file: &'static str,
81    /// Its program: what it is called and ordered as too (see [`titled`]).
82    pub program: Program,
83    /// The datum plane through the face it sits on — `seat`, `base`, ...
84    /// — which a coincident mate picks; its datum `axis` the concentric
85    /// one.
86    pub base: &'static str,
87    /// The faces a thread goes on or in — a screw's shank, a nut's or a
88    /// standoff's bore — by name; none for a part without one.
89    pub threaded: Vec<String>,
90}
91
92/// A family's `program`, known as `title` — the norm and the name — and a
93/// size of it ordered as `designation` and the size: the norm, `ISO 4762`
94/// for `ISO 4762 M4x12`, or the catalogue name. Every consumer of a placed
95/// part reads them from its program (see
96/// [`geop_ops_model::parameters::Properties`]).
97fn titled(mut program: Program, title: &str, designation: &str) -> Program {
98    let mut parameters = parameters_of(&program);
99    parameters.title = Some(title.into());
100    parameters.designation = Some(designation.into());
101    set_parameters(&mut program, parameters);
102    program
103}
104
105/// A built family, and its program as the text a file holds.
106struct Entry {
107    part: StandardPart,
108    text: String,
109}
110
111/// What builds a family, given the file it is placed from.
112type Family = fn(&'static str) -> GeopResult<StandardPart>;
113
114/// Every family: the file it is placed from, and what builds it. Listing
115/// the files builds nothing; each family is built the first time it is
116/// read (see [`entry`]).
117const FAMILIES: [(&str, Family); 25] = [
118    ("std:iso4762_socket_head_cap_screw.geop", fasteners::iso4762),
119    ("std:iso7380_button_head_screw.geop", fasteners::iso7380),
120    ("std:iso10642_countersunk_screw.geop", fasteners::iso10642),
121    ("std:iso4017_hex_head_screw.geop", fasteners::iso4017),
122    ("std:iso4032_hex_nut.geop", fasteners::iso4032),
123    ("std:iso10511_nylon_insert_nut.geop", fasteners::iso10511),
124    ("std:iso7089_washer.geop", fasteners::iso7089),
125    ("std:iso7090_chamfered_washer.geop", fasteners::iso7090),
126    ("std:iso8734_dowel_pin.geop", fasteners::iso8734),
127    ("std:hex_standoff.geop", fasteners::hex_standoff),
128    ("std:ball_bearing.geop", components::ball_bearing),
129    ("std:tslot_2020.geop", components::tslot_2020),
130    ("std:tslot_2040.geop", components::tslot_2040),
131    ("std:nema17_stepper.geop", components::nema17),
132    ("std:spur_gear.geop", drive::spur_gear),
133    ("std:gt2_pulley_16t.geop", drive::gt2_pulley_16),
134    ("std:gt2_pulley_20t.geop", drive::gt2_pulley_20),
135    ("std:gt2_pulley_36t.geop", drive::gt2_pulley_36),
136    ("std:shaft_collar.geop", drive::shaft_collar),
137    ("std:flange_coupling.geop", drive::flange_coupling),
138    ("std:gear_rack.geop", drive::rack),
139    ("std:linear_rail.geop", components::linear_rail),
140    ("std:linear_carriage.geop", components::linear_carriage),
141    ("std:servo_sg90.geop", components::servo_sg90),
142    ("std:servo_mg996r.geop", components::servo_mg996r),
143];
144
145/// The family placed from `file`, built once, the first time it is asked
146/// for: or why it would not build. `None` for a file that is no standard
147/// part.
148fn entry(file: &str) -> Option<GeopResult<&'static Entry>> {
149    static ENTRIES: [OnceLock<GeopResult<Entry>>; FAMILIES.len()] =
150        [const { OnceLock::new() }; FAMILIES.len()];
151    let k = FAMILIES.iter().position(|(f, _)| *f == file)?;
152    let (file, family) = FAMILIES[k];
153    let built = ENTRIES[k].get_or_init(|| {
154        let part = family(file)
155            .map_err(|e| e.with_context(format!("building the standard part {file}")))?;
156        let text = (part.program.to_json())
157            .map_err(|e| e.with_context(format!("writing the standard part {file}")))?;
158        Ok(Entry { part, text })
159    });
160    Some(built.as_ref().map_err(GeopError::clone))
161}
162
163/// Every standard part, each built if it was not yet.
164pub fn parts() -> GeopResult<impl Iterator<Item = &'static StandardPart>> {
165    let parts = FAMILIES
166        .iter()
167        .map(|(file, _)| part(file))
168        .collect::<GeopResult<Vec<_>>>()?;
169    Ok(parts.into_iter())
170}
171
172/// The standard part placed from `file`.
173pub fn part(file: &str) -> GeopResult<&'static StandardPart> {
174    Ok(&entry(file).ok_or_else(|| no_such_part(file))??.part)
175}
176
177fn no_such_part(file: &str) -> GeopError {
178    let known: Vec<&str> = FAMILIES.iter().map(|(f, _)| *f).collect();
179    GeopError::new(format!(
180        "there is no standard part {file:?}: there are {}",
181        known.join(", ")
182    ))
183}
184
185/// The program files `F`, with the standard parts beside them: every file
186/// named [`PREFIX`]`…` is a standard part's — read-only, whatever `F`
187/// holds by that name — and every other is `F`'s.
188#[derive(Clone, Debug, Default)]
189pub struct WithStandardParts<F>(pub F);
190
191impl<F> Deref for WithStandardParts<F> {
192    type Target = F;
193
194    fn deref(&self) -> &F {
195        &self.0
196    }
197}
198
199impl<F> DerefMut for WithStandardParts<F> {
200    fn deref_mut(&mut self) -> &mut F {
201        &mut self.0
202    }
203}
204
205impl<F: FileStore> FileStore for WithStandardParts<F> {
206    fn read(&self, path: &str) -> GeopResult<String> {
207        if !path.starts_with(PREFIX) {
208            return self.0.read(path);
209        }
210        Ok(entry(path).ok_or_else(|| no_such_part(path))??.text.clone())
211    }
212
213    fn list(&self) -> Vec<String> {
214        let mut files: Vec<String> = self
215            .0
216            .list()
217            .into_iter()
218            .filter(|f| !f.starts_with(PREFIX))
219            .collect();
220        files.extend(FAMILIES.iter().map(|(file, _)| file.to_string()));
221        files
222    }
223}
224
225/// Writes go to `F`, but never over a standard part: those are read-only,
226/// so a write to a [`PREFIX`]`…` name changes nothing.
227impl<F: FileStoreMut> FileStoreMut for WithStandardParts<F> {
228    fn write(&mut self, path: &str, text: Option<String>) {
229        if !path.starts_with(PREFIX) {
230            self.0.write(path, text);
231        }
232    }
233}