Skip to main content

geop_ops_model/spacing/
mod.rs

1//! Where the instances of a pattern go — the bodies or features of a
2//! pattern, the copies of a placed part, fasteners in holes — and the
3//! fields that set it: [`Frames`], every way of laying instances out, each
4//! the motions from the seed to its instances; [`Spacing`], the [`Row`]
5//! of instances in a line or round an axis, and the count
6//! ([`count_field`], [`whole_count`]); what they are laid out along, round
7//! or at ([`references`]); and where a part's holes are ([`holes`]).
8//!
9//! Every number here is a [`NumericInput`]: plain, or a formula of the
10//! part's parameters, resolved through whatever reader the step or its
11//! form reads the part with.
12
13use 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/// How far apart the instances of a pattern are: each `Step` from the
35/// last, or spread evenly over the `Extent` from the first to the last —
36/// a length, or an angle in degrees, plain or a formula of the part's
37/// parameters. Serialized as `{"step": 2.0}`, `{"extent": 10.0}` or
38/// `{"step": {"formula": "pitch"}}`.
39#[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    /// Each `step` — a number, or a formula's text — from the last.
48    pub fn step(step: impl Into<NumericInput>) -> Self {
49        Spacing::Step(step.into())
50    }
51
52    /// Spread evenly over `extent` — a number, or a formula's text.
53    pub fn extent(extent: impl Into<NumericInput>) -> Self {
54        Spacing::Extent(extent.into())
55    }
56
57    /// The number it holds, whichever it is.
58    pub fn value(&self) -> &NumericInput {
59        match self {
60            Spacing::Step(v) | Spacing::Extent(v) => v,
61        }
62    }
63
64    /// The same kind of spacing, holding `value`.
65    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    /// `count` instances in a row along a line, this spacing coming to a
73    /// length as `reader` reads the parameters. Refuses a spacing that puts
74    /// them on top of each other.
75    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    /// `count` instances in a row round an axis, this spacing coming to an
94    /// angle in degrees as `reader` reads the parameters — an extent of a
95    /// full turn spreading them over it, the last a step short of the
96    /// first. Refuses a spacing that puts them on top of each other, or
97    /// brings them round onto the first again.
98    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    /// Its fields in `f`: which kind — keyed `{key}_mode` — and the number,
130    /// keyed `key`, made by `number(label, value)` (see
131    /// [`NumericInput::number`]). Switching the kind keeps the number. `set`
132    /// puts the spacing set where the arguments keep it.
133    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/// Instances of a pattern in a row: `count` of them, the first where the
175/// seed is and the `k`-th `value * k / divisor` on from it — a length, or
176/// an angle in degrees (see [`Spacing::along`], [`Spacing::round`]).
177#[derive(Clone, Copy, Debug, PartialEq)]
178pub struct Row {
179    pub count: usize,
180    value: f64,
181    divisor: usize,
182}
183
184impl Row {
185    /// How far apart two neighbouring instances are, in plain numbers.
186    pub fn step(&self) -> f64 {
187        self.value / self.divisor as f64
188    }
189
190    /// How far instance `k` is from the first, `value * k / divisor`
191    /// enclosed in one go rather than accumulated step by step.
192    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    /// Where instance `k` is relative to the first, along the unit vector
202    /// `unit`: none for the first itself.
203    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    /// The motion turning the first instance to instance `k`, round the
211    /// axis through `point` along the unit vector `direction`.
212    pub fn turn(&self, point: &Vector3, direction: &Vector3, k: usize) -> GeopResult<Pose> {
213        // In radians, enclosed in one go.
214        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
222/// The count field of a pattern in `f`, keyed `key`, of `count` as
223/// `reader` reads the parameters — dragged by `handle` unless it follows a
224/// formula — calling `set` with the count it is set to: a formula as
225/// typed, a number rounded to a whole one of at least one, the seed alone.
226pub 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
246/// What a count field set to `value` makes the count (see [`count_field`]).
247fn 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
257/// The number of instances `count` comes to as `reader` reads the
258/// parameters: a whole number, at least one — the seed alone. Anything else
259/// is refused, saying so.
260pub 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}