geop_ops_model/
threads.rs1use 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#[derive(Clone, Debug, PartialEq)]
37pub struct CosmeticThread {
38 pub designation: String,
40 pub face: String,
42 pub axis: Axis,
45 pub radius: Scalar,
48 pub taper: Scalar,
53 pub major_diameter: f64,
56 pub minor_diameter: f64,
57 pub pitch: f64,
58 pub length: f64,
60 pub internal: bool,
62 pub handedness: Handedness,
63}
64
65impl CosmeticThread {
66 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 pub fn polyline(&self) -> GeopResult<Vec<Vector3>> {
80 let helix = self.helix()?;
81 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
90const SEGMENTS_PER_SPAN: usize = 8;
93
94#[derive(Clone, Debug, Default, PartialEq)]
97pub struct Threads(BTreeMap<String, CosmeticThread>);
98
99pub fn threads() -> TargetReference<Threads> {
101 TargetReference::new("threads")
102}
103
104impl Threads {
105 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 pub fn iter(&self) -> impl Iterator<Item = (&str, &CosmeticThread)> {
120 self.0.iter().map(|(name, thread)| (name.as_str(), thread))
121 }
122
123 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 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 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
156impl Target for Threads {
158 const CHAIN: bool = true;
159}
160
161impl 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
175pub 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
185pub fn draw_threads(ctx: &mut StepContext) -> GeopResult<()> {
188 let step = ctx.step();
189 ctx.produce(&annotations(step), &step.to_string(), draw)
190}
191
192#[allow(clippy::ptr_arg)]
195fn draw(reader: &Reader, step: &String) -> GeopResult<Annotations> {
196 Ok(Annotations(read_threads(reader)?.drawn(step)?))
197}