1use geop_core_editor::{
14 edit_form::{Edit, EditForm},
15 editing::{Choice, Number, Track, Unit, Value},
16};
17use geop_core_math::{
18 geop_error::{GeopError, GeopResult},
19 primitives::Pose,
20 scalars::Scalar,
21 vector::Vector3,
22};
23use geop_core_program::{numeric::NumericInput, reader::Reader};
24use serde::{Deserialize, Serialize};
25
26mod frames;
27pub mod holes;
28pub mod references;
29
30pub use frames::{Direction, Frames, Offset, track};
31pub use holes::{HoleFit, HoleSite, HoleSites, hole_sites, holes_of, pose_of, read_hole_sites};
32pub use references::{PatternDirection, PointsRef, TurnAxis, resolve_points};
33
34#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
40#[serde(rename_all = "snake_case")]
41pub enum Spacing {
42 Step(NumericInput),
43 Extent(NumericInput),
44}
45
46impl Spacing {
47 pub fn step(step: impl Into<NumericInput>) -> Self {
49 Spacing::Step(step.into())
50 }
51
52 pub fn extent(extent: impl Into<NumericInput>) -> Self {
54 Spacing::Extent(extent.into())
55 }
56
57 pub fn value(&self) -> &NumericInput {
59 match self {
60 Spacing::Step(v) | Spacing::Extent(v) => v,
61 }
62 }
63
64 fn with_value(&self, value: NumericInput) -> Self {
66 match self {
67 Spacing::Step(_) => Spacing::Step(value),
68 Spacing::Extent(_) => Spacing::Extent(value),
69 }
70 }
71
72 pub fn along(&self, count: usize, reader: &Reader) -> GeopResult<Row> {
76 let value = self.value().resolve(reader)?;
77 if !value.is_finite() || value == 0.0 {
78 return Err(GeopError::new(format!(
79 "the spacing {value} puts every copy on top of the bodies"
80 )));
81 }
82 let divisor = match self {
83 Spacing::Step(_) => 1,
84 Spacing::Extent(_) => (count - 1).max(1),
85 };
86 Ok(Row {
87 count,
88 value,
89 divisor,
90 })
91 }
92
93 pub fn round(&self, count: usize, reader: &Reader) -> GeopResult<Row> {
99 let value = self.value().resolve(reader)?;
100 if !value.is_finite() || value == 0.0 {
101 return Err(GeopError::new(format!(
102 "the angle {value}° puts every copy on top of the bodies"
103 )));
104 }
105 let span = |step: f64| step.abs() * (count - 1) as f64;
106 let divisor = match self {
107 Spacing::Extent(_) if value.abs() == 360.0 => count,
108 Spacing::Extent(_) if value.abs() > 360.0 => {
109 return Err(GeopError::new(format!(
110 "the pattern spans {value}°, more than a full turn: copies would come round onto each other"
111 )));
112 }
113 Spacing::Extent(_) => (count - 1).max(1),
114 Spacing::Step(_) if span(value) >= 360.0 => {
115 return Err(GeopError::new(format!(
116 "{} steps of {value}° come round onto the bodies again: spread the instances over 360° instead",
117 count - 1
118 )));
119 }
120 Spacing::Step(_) => 1,
121 };
122 Ok(Row {
123 count,
124 value,
125 divisor,
126 })
127 }
128
129 pub fn fields<'a, A, T>(
134 &self,
135 f: &mut EditForm<'a, A, T>,
136 key: &str,
137 labels: [&str; 2],
138 number: impl FnOnce(&str, &NumericInput) -> Number,
139 set: impl Fn(&mut A, Spacing) + Clone + 'a,
140 ) {
141 let (mode, label) = match self {
142 Spacing::Step(_) => ("step", labels[0]),
143 Spacing::Extent(_) => ("extent", labels[1]),
144 };
145 let modes = vec![
146 Choice::new("step", labels[0]),
147 Choice::new("extent", labels[1]),
148 ];
149 let (value, kind) = (self.value().clone(), self.clone());
150 let set_mode = set.clone();
151 f.select(
152 &format!("{key}_mode"),
153 "spacing",
154 mode,
155 modes,
156 false,
157 move |edit, mode| {
158 let number = value.clone();
159 set_mode(
160 edit.args,
161 match mode {
162 "extent" => Spacing::Extent(number),
163 _ => Spacing::Step(number),
164 },
165 );
166 },
167 )
168 .numeric(key, number(label, self.value()), move |edit, number| {
169 set(edit.args, kind.with_value(number))
170 });
171 }
172}
173
174#[derive(Clone, Copy, Debug, PartialEq)]
178pub struct Row {
179 pub count: usize,
180 value: f64,
181 divisor: usize,
182}
183
184impl Row {
185 pub fn step(&self) -> f64 {
187 self.value / self.divisor as f64
188 }
189
190 pub fn distance(&self, k: usize) -> GeopResult<Scalar> {
193 let distance = Scalar::from_f64(self.value).mul(Scalar::from_i64(k as i64));
194 if self.divisor == 1 {
195 Ok(distance)
196 } else {
197 distance.div(Scalar::from_i64(self.divisor as i64))
198 }
199 }
200
201 pub fn offset(&self, unit: &Vector3, k: usize) -> GeopResult<Option<Vector3>> {
204 if k == 0 {
205 return Ok(None);
206 }
207 Ok(Some(unit.prod_scalar(self.distance(k)?)))
208 }
209
210 pub fn turn(&self, point: &Vector3, direction: &Vector3, k: usize) -> GeopResult<Pose> {
213 let radians = Scalar::from_f64(self.value)
215 .mul(Scalar::from_i64(k as i64))
216 .mul(Scalar::PI)
217 .div(Scalar::from_i64(180 * self.divisor as i64))?;
218 Pose::rotation_about(point, direction, radians)
219 }
220}
221
222pub fn count_field<'a, A, T>(
227 f: &mut EditForm<'a, A, T>,
228 key: &str,
229 count: &NumericInput,
230 reader: &Reader,
231 handle: Option<Track>,
232 set: impl Fn(Edit<'_, A, T>, NumericInput) + 'a,
233) {
234 let mut number = Number::numeric(count, "count", reader, Unit::Count)
235 .range(1.0, 24.0)
236 .handle(handle);
237 number.step = 1.0;
238 f.form.dialog.number(key, number);
239 f.on(key, move |edit, value| {
240 if let Some(count) = count_of(&value) {
241 set(edit, count);
242 }
243 });
244}
245
246fn count_of(value: &Value) -> Option<NumericInput> {
248 Some(match value.numeric_input()? {
249 NumericInput::Plain(value) if value.is_finite() && value >= 1.0 => {
250 NumericInput::Plain(value.round())
251 }
252 NumericInput::Plain(_) => NumericInput::Plain(1.0),
253 formula => formula,
254 })
255}
256
257pub fn whole_count(count: &NumericInput, reader: &Reader) -> GeopResult<usize> {
261 let value = count.resolve(reader)?;
262 if value.fract() != 0.0 || value < 1.0 {
263 return Err(GeopError::new(format!(
264 "the count {count} comes to {value}: a pattern has a whole number of instances, at least one — round a formula with round(), floor() or ceil()"
265 )));
266 }
267 Ok(value as usize)
268}