Skip to main content

geop_ops_model/spacing/
frames.rs

1//! [`Frames`]: where the instances of a pattern go, from whatever gives
2//! them — a row or a grid, a turn round an axis, a curve, points, a table
3//! of offsets, a face filled, the holes of features — each the motion from
4//! where the seed is to an instance. A pattern of bodies, of features and
5//! of placed parts lays its instances out by one of these; linear and
6//! circular are two of them.
7//!
8//! What a source gives that is a place rather than a motion — a point, a
9//! hole — is reached from where the seed is: the first of the points or
10//! holes, unless the seed is said to be elsewhere (a point picked `from`);
11//! a face is filled on a grid through the seed's anchor.
12
13use geop_core_editor::{
14    edit_form::{Edit, EditForm},
15    editing::{Cell, Choice, Column, ColumnKind, Number, Table, TableEdit, TableRow, Track, Unit},
16};
17use geop_core_geometry::{
18    contains::surface::surface_could_contain,
19    nurb_curve::NurbCurve3D,
20    search::{MAX_NODES, min_subdivision_size},
21};
22use geop_core_math::{
23    geop_error::{GeopError, GeopResult, WithContext},
24    primitives::{CoordinateSystem, FrameAxis, Pose, Quaternion},
25    scalars::Scalar,
26    vector::Vector3,
27    with_context,
28};
29use geop_core_program::{element::ElementReference, numeric::NumericInput, reader::Reader};
30use geop_core_topology::{
31    contains::face::{PointClassification, face_contains},
32    distance::{Feature as DistanceTo, closest_points},
33};
34use serde::{Deserialize, Serialize};
35
36use super::{
37    Row, Spacing, count_field,
38    holes::{holes_of, pose_of},
39    references::{PatternDirection, PointsRef, TurnAxis, resolve_points},
40    whole_count,
41};
42use crate::{
43    FeatureRef, NamedModel, PathRef, Plane, datums::origin_axis, parameters::located, read_model,
44};
45
46/// A handle at `at`, dragged along `direction`; none for a direction that
47/// could be zero, which nothing could be dragged along.
48pub fn track(at: Vector3, direction: Vector3) -> Option<Track> {
49    let length = direction.norm_sq();
50    length
51        .definitely_greater(Scalar::ZERO)
52        .then_some(Track { at, direction })
53}
54
55/// One direction of a row or a grid of instances.
56#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
57pub struct Direction {
58    /// What gives the direction: a line — a straight edge, an axis, a
59    /// sketch line — or a plane, along its normal; none, for a step that
60    /// has not picked one.
61    pub along: Option<PatternDirection>,
62    /// Go against it instead.
63    #[serde(default)]
64    pub reversed: bool,
65    /// How many instances along it, the seed included: a number, or a
66    /// formula of the part's parameters.
67    pub count: NumericInput,
68    /// How far apart they are.
69    pub spacing: Spacing,
70}
71
72impl Direction {
73    /// Along the origin's axis `axis`, three instances a unit apart.
74    pub fn along(axis: FrameAxis) -> Self {
75        Direction {
76            along: Some(PatternDirection::Datum(origin_axis(axis))),
77            reversed: false,
78            count: NumericInput::Plain(3.0),
79            spacing: Spacing::step(1.0),
80        }
81    }
82
83    /// The unit vector it goes along, as `reader` reads the part.
84    pub fn unit(&self, reader: &Reader) -> GeopResult<Vector3> {
85        let along = (self.along.as_ref())
86            .ok_or_else(|| GeopError::new("pick a direction to pattern along"))?;
87        let d = along.direction(reader)?;
88        Ok(if self.reversed { d.neg() } else { d })
89    }
90
91    /// The instances along it, as `reader` reads the parameters.
92    pub fn row(&self, reader: &Reader) -> GeopResult<Row> {
93        let count = whole_count(&self.count, reader)?;
94        self.spacing.along(count, reader)
95    }
96
97    /// Its fields in `f`, keyed with `suffix`: what it goes along, its
98    /// sense, the count and the spacing, with handles from `at`. `set` puts
99    /// the direction set where the arguments keep it.
100    pub fn fields<'a, A: 'a>(
101        &self,
102        f: &mut EditForm<'a, A>,
103        before: &Reader<'a>,
104        at: Option<Vector3>,
105        suffix: &str,
106        set: impl Fn(&mut A, Direction) + Clone + 'a,
107    ) {
108        let key = |name: &str| format!("{name}{suffix}");
109        let set_along = with(self.clone(), set.clone(), |d, picked| {
110            d.along = Some(picked)
111        });
112        let set_reversed = with(self.clone(), set.clone(), |d, b| d.reversed = b);
113        f.pick_one(
114            &key("direction"),
115            "direction",
116            before,
117            self.along.as_ref(),
118            move |edit, p| set_along(edit.args, p),
119        )
120        .checkbox(
121            &key("reversed"),
122            "reverse direction",
123            self.reversed,
124            move |edit, b| set_reversed(edit.args, b),
125        );
126        // Moving the spacing's handle by the unit direction adds one to it;
127        // the count's, by one step, adds an instance.
128        let unit = self.unit(before).ok();
129        let row = self.row(before).ok();
130        let handles = at.zip(unit);
131        let count_handle =
132            handles
133                .zip(row.filter(|row| row.count > 1))
134                .and_then(|((at, unit), row)| {
135                    let step = row.step();
136                    let end = (row.count - 1) as f64 * step;
137                    track(
138                        at.add(&unit.prod_scalar(Scalar::from_f64(end))),
139                        unit.prod_scalar(Scalar::from_f64(step)),
140                    )
141                });
142        let set_count = with(self.clone(), set.clone(), |d, count| d.count = count);
143        count_field(
144            f,
145            &key("count"),
146            &self.count,
147            before,
148            count_handle,
149            move |edit, count| set_count(edit.args, count),
150        );
151        self.spacing.fields(
152            f,
153            &key("spacing"),
154            ["spacing", "total"],
155            |label, value| {
156                let number = Number::numeric(value, label, before, Unit::Length);
157                let at = handles.and_then(|(at, unit)| {
158                    track(
159                        at.add(&unit.prod_scalar(Scalar::from_f64(number.value))),
160                        unit,
161                    )
162                });
163                number.handle(at)
164            },
165            with(self.clone(), set.clone(), |d, spacing| d.spacing = spacing),
166        );
167    }
168}
169
170/// What setting one field of `value` to a `V` makes of it, put where the
171/// arguments keep it by `set`.
172fn with<A, T: Clone, V>(
173    value: T,
174    set: impl Fn(&mut A, T) + Clone,
175    change: fn(&mut T, V),
176) -> impl Fn(&mut A, V) + Clone {
177    move |args, v| {
178        let mut value = value.clone();
179        change(&mut value, v);
180        set(args, value);
181    }
182}
183
184/// One instance of a [`Frames::Table`]: how far it is from the seed along
185/// each axis — numbers, or formulas of the part's parameters.
186#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
187pub struct Offset {
188    pub x: NumericInput,
189    pub y: NumericInput,
190    pub z: NumericInput,
191}
192
193/// Where the instances of a pattern go (see the module's docs).
194#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
195#[serde(tag = "type", rename_all = "snake_case")]
196pub enum Frames {
197    /// In a row along a direction, or in a grid along two.
198    Linear {
199        first: Direction,
200        #[serde(default, skip_serializing_if = "Option::is_none")]
201        second: Option<Direction>,
202    },
203    /// Turned round an axis: counter-clockwise looking against it, the
204    /// other way if `reversed`.
205    Circular {
206        axis: Option<TurnAxis>,
207        #[serde(default)]
208        reversed: bool,
209        count: NumericInput,
210        /// How far apart, in degrees.
211        angle: Spacing,
212    },
213    /// Along a path from where it starts — the seed where it starts — the
214    /// instances `spacing` apart along it, or spread over a length; each
215    /// turned as the path turns, if `aligned`, or kept as the seed is.
216    Curve {
217        path: Option<PathRef>,
218        count: NumericInput,
219        spacing: Spacing,
220        #[serde(default)]
221        aligned: bool,
222    },
223    /// At points: the seed at the first, or where `from` is.
224    Points {
225        points: Vec<PointsRef>,
226        #[serde(default, skip_serializing_if = "Option::is_none")]
227        from: Option<PointsRef>,
228    },
229    /// At offsets from the seed, a row each.
230    Table { rows: Vec<Offset> },
231    /// Filling a flat face on a grid through the seed — square, or each
232    /// row shifted half a step and the rows closer — every instance
233    /// `margin` or more inside the face's edges.
234    Fill {
235        face: Option<ElementReference<NamedModel, Plane>>,
236        spacing: NumericInput,
237        #[serde(default)]
238        margin: NumericInput,
239        #[serde(default)]
240        staggered: bool,
241    },
242    /// At the holes features drilled: the seed in the first, each copy in
243    /// another, turned as the hole is.
244    Holes { features: Vec<FeatureRef> },
245}
246
247/// The kinds of [`Frames`], as a dialog offers them: the value, the label.
248const KINDS: [(&str, &str); 7] = [
249    ("linear", "In a row or a grid"),
250    ("circular", "Round an axis"),
251    ("curve", "Along a curve"),
252    ("points", "At points"),
253    ("table", "From a table of offsets"),
254    ("fill", "Filling a face"),
255    ("holes", "At holes"),
256];
257
258/// The most instances a pattern lays out: a fill spaced finer than its
259/// face could otherwise make millions.
260const MOST: usize = 10_000;
261
262impl Frames {
263    /// Its kind, as [`KINDS`] has it.
264    fn kind(&self) -> &'static str {
265        match self {
266            Frames::Linear { .. } => "linear",
267            Frames::Circular { .. } => "circular",
268            Frames::Curve { .. } => "curve",
269            Frames::Points { .. } => "points",
270            Frames::Table { .. } => "table",
271            Frames::Fill { .. } => "fill",
272            Frames::Holes { .. } => "holes",
273        }
274    }
275
276    /// A fresh one of the kind `kind`, nothing picked.
277    fn fresh(kind: &str) -> Self {
278        match kind {
279            "circular" => Frames::Circular {
280                axis: Some(TurnAxis::Datum(origin_axis(FrameAxis::Z))),
281                reversed: false,
282                count: NumericInput::Plain(4.0),
283                angle: Spacing::extent(360.0),
284            },
285            "curve" => Frames::Curve {
286                path: None,
287                count: NumericInput::Plain(4.0),
288                spacing: Spacing::step(10.0),
289                aligned: false,
290            },
291            "points" => Frames::Points {
292                points: Vec::new(),
293                from: None,
294            },
295            "table" => Frames::Table {
296                rows: vec![Offset {
297                    x: NumericInput::Plain(10.0),
298                    ..Offset::default()
299                }],
300            },
301            "fill" => Frames::Fill {
302                face: None,
303                spacing: NumericInput::Plain(10.0),
304                margin: NumericInput::Plain(0.0),
305                staggered: false,
306            },
307            "holes" => Frames::Holes {
308                features: Vec::new(),
309            },
310            _ => Frames::Linear {
311                first: Direction::along(FrameAxis::X),
312                second: None,
313            },
314        }
315    }
316
317    /// Where its instances go, as `reader` reads the part: each a label
318    /// and the motion from the seed, the seed itself left out. `anchor` is
319    /// where the seed is, for a fill: asked only there.
320    pub fn motions(
321        &self,
322        reader: &Reader,
323        anchor: &dyn Fn() -> GeopResult<Vector3>,
324    ) -> GeopResult<Vec<(String, Pose)>> {
325        let motions = match self {
326            Frames::Linear { first, second } => linear(reader, first, second.as_ref()),
327            Frames::Circular {
328                axis,
329                reversed,
330                count,
331                angle,
332            } => circular(reader, axis.as_ref(), *reversed, count, angle),
333            Frames::Curve {
334                path,
335                count,
336                spacing,
337                aligned,
338            } => along_curve(reader, path.as_ref(), count, spacing, *aligned),
339            Frames::Points { points, from } => at_points(reader, points, from.as_ref()),
340            Frames::Table { rows } => from_table(reader, rows),
341            Frames::Fill {
342                face,
343                spacing,
344                margin,
345                staggered,
346            } => fill(reader, face.as_ref(), spacing, margin, *staggered, anchor),
347            Frames::Holes { features } => at_holes(reader, features),
348        }?;
349        if motions.len() > MOST {
350            return Err(GeopError::new(format!(
351                "{} instances, more than the {MOST} a pattern lays out: space them further apart",
352                motions.len()
353            )));
354        }
355        Ok(motions)
356    }
357
358    /// Its fields in `f`: which kind — a choice keyed `frames` — and that
359    /// kind's own, picked in the part `before` reads; handles of a row's
360    /// from `at`. `set` puts the frames set where the arguments keep them.
361    pub fn fields<'a, A: 'a>(
362        &self,
363        f: &mut EditForm<'a, A>,
364        before: &Reader<'a>,
365        at: Option<Vector3>,
366        set: impl Fn(&mut A, Frames) + Clone + 'a,
367    ) {
368        let choices = KINDS.iter().map(|&(v, l)| Choice::new(v, l)).collect();
369        let (own, set_kind) = (self.kind(), set.clone());
370        f.select(
371            "frames",
372            "pattern",
373            own,
374            choices,
375            false,
376            move |edit, kind| {
377                if kind != own {
378                    set_kind(edit.args, Frames::fresh(kind));
379                }
380            },
381        );
382        match self {
383            Frames::Linear { first, second } => {
384                let second = second.clone();
385                let set_first = set.clone();
386                first.fields(f, before, at, "", move |args, first| {
387                    set_first(
388                        args,
389                        Frames::Linear {
390                            first,
391                            second: second.clone(),
392                        },
393                    )
394                });
395                let (first, set_grid) = (first.clone(), set.clone());
396                let has_second = matches!(
397                    self,
398                    Frames::Linear {
399                        second: Some(_),
400                        ..
401                    }
402                );
403                let grid_first = first.clone();
404                f.checkbox("grid", "second direction", has_second, move |edit, on| {
405                    set_grid(
406                        edit.args,
407                        Frames::Linear {
408                            first: grid_first.clone(),
409                            second: on.then(|| Direction::along(FrameAxis::Y)),
410                        },
411                    )
412                });
413                if let Frames::Linear {
414                    second: Some(second),
415                    ..
416                } = self
417                {
418                    second.fields(f, before, at, "2", move |args, second| {
419                        set(
420                            args,
421                            Frames::Linear {
422                                first: first.clone(),
423                                second: Some(second),
424                            },
425                        )
426                    });
427                }
428            }
429            Frames::Circular { .. } => self.circular_fields(f, before, set),
430            Frames::Curve { .. } => self.curve_fields(f, before, set),
431            Frames::Points { points, from } => {
432                let (from_kept, set_points) = (from.clone(), set.clone());
433                f.pick(
434                    "points",
435                    "points",
436                    before,
437                    points,
438                    true,
439                    move |edit, points| {
440                        set_points(
441                            edit.args,
442                            Frames::Points {
443                                points,
444                                from: from_kept.clone(),
445                            },
446                        )
447                    },
448                );
449                let points = points.clone();
450                f.pick_one(
451                    "from",
452                    "seed at",
453                    before,
454                    from.as_ref(),
455                    move |edit, from| {
456                        set(
457                            edit.args,
458                            Frames::Points {
459                                points: points.clone(),
460                                from: Some(from),
461                            },
462                        )
463                    },
464                );
465                // The seed is at the first point unless said otherwise.
466                f.form.dialog.optional("from");
467            }
468            Frames::Table { rows } => table_fields(f, before, rows, set),
469            Frames::Fill { .. } => self.fill_fields(f, before, set),
470            Frames::Holes { features } => {
471                f.pick(
472                    "holes",
473                    "holes",
474                    before,
475                    features,
476                    true,
477                    move |edit, features| set(edit.args, Frames::Holes { features }),
478                );
479            }
480        }
481    }
482
483    /// The fields of a turn round an axis.
484    fn circular_fields<'a, A: 'a>(
485        &self,
486        f: &mut EditForm<'a, A>,
487        before: &Reader<'a>,
488        set: impl Fn(&mut A, Frames) + Clone + 'a,
489    ) {
490        let Frames::Circular {
491            axis,
492            reversed,
493            count,
494            angle,
495        } = self.clone()
496        else {
497            return;
498        };
499        let me = self.clone();
500        let set_axis = set.clone();
501        let me_axis = me.clone();
502        f.pick_one(
503            "axis",
504            "axis",
505            before,
506            axis.as_ref(),
507            move |edit, picked| {
508                let mut frames = me_axis.clone();
509                if let Frames::Circular { axis, .. } = &mut frames {
510                    *axis = Some(picked);
511                }
512                set_axis(edit.args, frames)
513            },
514        );
515        let (set_rev, me_rev) = (set.clone(), me.clone());
516        f.checkbox("reversed", "reverse direction", reversed, move |edit, b| {
517            let mut frames = me_rev.clone();
518            if let Frames::Circular { reversed, .. } = &mut frames {
519                *reversed = b;
520            }
521            set_rev(edit.args, frames)
522        });
523        let (set_count, me_count) = (set.clone(), me.clone());
524        count_field(f, "count", &count, before, None, move |edit, c| {
525            let mut frames = me_count.clone();
526            if let Frames::Circular { count, .. } = &mut frames {
527                *count = c;
528            }
529            set_count(edit.args, frames)
530        });
531        angle.fields(
532            f,
533            "angle",
534            ["angle", "total angle"],
535            |label, value| Number::numeric(value, label, before, Unit::Angle).range(-360.0, 360.0),
536            move |args, a| {
537                let mut frames = me.clone();
538                if let Frames::Circular { angle, .. } = &mut frames {
539                    *angle = a;
540                }
541                set(args, frames)
542            },
543        );
544    }
545
546    /// The fields of a pattern along a curve.
547    fn curve_fields<'a, A: 'a>(
548        &self,
549        f: &mut EditForm<'a, A>,
550        before: &Reader<'a>,
551        set: impl Fn(&mut A, Frames) + Clone + 'a,
552    ) {
553        let Frames::Curve {
554            path,
555            count,
556            spacing,
557            aligned,
558        } = self.clone()
559        else {
560            return;
561        };
562        let me = self.clone();
563        let (set_path, me_path) = (set.clone(), me.clone());
564        f.pick_one(
565            "path",
566            "along",
567            before,
568            path.as_ref(),
569            move |edit, picked| {
570                let mut frames = me_path.clone();
571                if let Frames::Curve { path, .. } = &mut frames {
572                    *path = Some(picked);
573                }
574                set_path(edit.args, frames)
575            },
576        );
577        let (set_count, me_count) = (set.clone(), me.clone());
578        count_field(f, "count", &count, before, None, move |edit, c| {
579            let mut frames = me_count.clone();
580            if let Frames::Curve { count, .. } = &mut frames {
581                *count = c;
582            }
583            set_count(edit.args, frames)
584        });
585        let (set_spacing, me_spacing) = (set.clone(), me.clone());
586        spacing.fields(
587            f,
588            "spacing",
589            ["spacing", "total"],
590            |label, value| Number::numeric(value, label, before, Unit::Length),
591            move |args, s| {
592                let mut frames = me_spacing.clone();
593                if let Frames::Curve { spacing, .. } = &mut frames {
594                    *spacing = s;
595                }
596                set_spacing(args, frames)
597            },
598        );
599        f.checkbox("aligned", "turn with the curve", aligned, move |edit, b| {
600            let mut frames = me.clone();
601            if let Frames::Curve { aligned, .. } = &mut frames {
602                *aligned = b;
603            }
604            set(edit.args, frames)
605        });
606    }
607
608    /// The fields of a face filled.
609    fn fill_fields<'a, A: 'a>(
610        &self,
611        f: &mut EditForm<'a, A>,
612        before: &Reader<'a>,
613        set: impl Fn(&mut A, Frames) + Clone + 'a,
614    ) {
615        let Frames::Fill {
616            face,
617            spacing,
618            margin,
619            staggered,
620        } = self.clone()
621        else {
622            return;
623        };
624        let me = self.clone();
625        let (set_face, me_face) = (set.clone(), me.clone());
626        f.pick_one(
627            "face",
628            "face",
629            before,
630            face.as_ref(),
631            move |edit, picked| {
632                let mut frames = me_face.clone();
633                if let Frames::Fill { face, .. } = &mut frames {
634                    *face = Some(picked);
635                }
636                set_face(edit.args, frames)
637            },
638        );
639        let (set_spacing, me_spacing) = (set.clone(), me.clone());
640        let number = Number::numeric(&spacing, "spacing", before, Unit::Length);
641        f.numeric("spacing", number, move |edit, s| {
642            let mut frames = me_spacing.clone();
643            if let Frames::Fill { spacing, .. } = &mut frames {
644                *spacing = s;
645            }
646            set_spacing(edit.args, frames)
647        });
648        let (set_margin, me_margin) = (set.clone(), me.clone());
649        let number = Number::numeric(&margin, "margin", before, Unit::Length);
650        f.numeric("margin", number, move |edit, m| {
651            let mut frames = me_margin.clone();
652            if let Frames::Fill { margin, .. } = &mut frames {
653                *margin = m;
654            }
655            set_margin(edit.args, frames)
656        });
657        f.checkbox("staggered", "staggered rows", staggered, move |edit, b| {
658            let mut frames = me.clone();
659            if let Frames::Fill { staggered, .. } = &mut frames {
660                *staggered = b;
661            }
662            set(edit.args, frames)
663        });
664    }
665}
666
667/// The fields of a table of offsets: a row per instance, a column per axis,
668/// each a number or a formula, what a formula comes to said beside it.
669fn table_fields<'a, A: 'a>(
670    f: &mut EditForm<'a, A>,
671    before: &Reader<'a>,
672    rows: &[Offset],
673    set: impl Fn(&mut A, Frames) + 'a,
674) {
675    let columns = ["x", "y", "z"]
676        .into_iter()
677        .map(|axis| Column::new(axis, ColumnKind::Formula))
678        .collect();
679    let cell = |value: &NumericInput| {
680        let cell = Cell::new(value.to_string());
681        match value {
682            NumericInput::Plain(_) => cell,
683            NumericInput::Formula { .. } => match value.resolve(before) {
684                Ok(v) => cell.detail(format!("= {v}"), geop_core_editor::editing::Tone::Hint),
685                Err(e) => cell.detail(e.to_string(), geop_core_editor::editing::Tone::Error),
686            },
687        }
688    };
689    let shown = (rows.iter())
690        .map(|row| TableRow::new(vec![cell(&row.x), cell(&row.y), cell(&row.z)]))
691        .collect();
692    let table = Table::new(columns, shown, "No instances: add a row.");
693    let rows = rows.to_vec();
694    f.table("rows", table, move |edit: Edit<'_, A, ()>, what| {
695        let mut rows = rows.clone();
696        match what {
697            TableEdit::Cell { row, column, text } => {
698                let Some(offset) = rows.get_mut(row) else {
699                    return;
700                };
701                let value = NumericInput::from(text.as_str());
702                match column {
703                    0 => offset.x = value,
704                    1 => offset.y = value,
705                    2 => offset.z = value,
706                    _ => return,
707                }
708            }
709            TableEdit::AddRow => rows.push(Offset::default()),
710            TableEdit::RemoveRow { row } if row < rows.len() => {
711                rows.remove(row);
712            }
713            TableEdit::MoveRow { row, to } if row < rows.len() && to < rows.len() => {
714                let moved = rows.remove(row);
715                rows.insert(to, moved);
716            }
717            _ => return,
718        }
719        set(edit.args, Frames::Table { rows });
720    });
721}
722
723/// A row along `first`, or a grid along it and `second`: instance `i`
724/// labelled `i`, in a grid `i.j`.
725fn linear(
726    reader: &Reader,
727    first: &Direction,
728    second: Option<&Direction>,
729) -> GeopResult<Vec<(String, Pose)>> {
730    let unit = first.unit(reader)?;
731    let first_row = first.row(reader)?;
732    let second = match second {
733        None => None,
734        Some(direction) => {
735            let across = direction.unit(reader)?;
736            let cross = unit.prod_cross(&across);
737            if (0..3).all(|k| cross[k].could_be_equal(Scalar::ZERO)) {
738                return Err(GeopError::new(
739                    "the two directions are parallel: the grid's rows would lie on top of each other",
740                ));
741            }
742            Some((across, direction.row(reader)?))
743        }
744    };
745    let rows = second.as_ref().map_or(1, |(_, row)| row.count);
746    let mut motions = Vec::new();
747    for i in 0..first_row.count {
748        for j in 0..rows {
749            let along = first_row.offset(&unit, i)?;
750            let across = match &second {
751                Some((across, row)) => row.offset(across, j)?,
752                None => None,
753            };
754            let offset = match (along, across) {
755                (None, None) => continue,
756                (Some(a), None) => a,
757                (None, Some(b)) => b,
758                (Some(a), Some(b)) => a.add(&b),
759            };
760            let label = match second {
761                Some(_) => format!("{i}.{j}"),
762                None => i.to_string(),
763            };
764            motions.push((label, Pose::new(offset, Quaternion::identity())?));
765        }
766    }
767    Ok(motions)
768}
769
770/// Turned round `axis`: instance `k` labelled `k`.
771fn circular(
772    reader: &Reader,
773    axis: Option<&TurnAxis>,
774    reversed: bool,
775    count: &NumericInput,
776    angle: &Spacing,
777) -> GeopResult<Vec<(String, Pose)>> {
778    let axis = axis
779        .ok_or_else(|| GeopError::new("pick an axis to turn around"))?
780        .placed(reader)?;
781    let direction = match reversed {
782        true => axis.direction.neg(),
783        false => axis.direction,
784    };
785    let row = angle.round(whole_count(count, reader)?, reader)?;
786    (1..row.count)
787        .map(|k| Ok((k.to_string(), row.turn(&axis.point, &direction, k)?)))
788        .collect()
789}
790
791/// Along `path` from where it starts: instance `k` labelled `k`, where
792/// `k` spacings of the row are run along it.
793fn along_curve(
794    reader: &Reader,
795    path: Option<&PathRef>,
796    count: &NumericInput,
797    spacing: &Spacing,
798    aligned: bool,
799) -> GeopResult<Vec<(String, Pose)>> {
800    let path = path.ok_or_else(|| GeopError::new("pick a curve to pattern along"))?;
801    let placed = path.resolve_placed(reader)?;
802    let motion = located(reader, &placed.instance)?.motion();
803    let curves: Vec<NurbCurve3D> = (placed.element.curves.iter())
804        .map(|c| c.transform(&motion))
805        .collect();
806    let lengths = (curves.iter())
807        .map(|c| Ok(c.length()?.0.to_f64()))
808        .collect::<GeopResult<Vec<f64>>>()?;
809    let total: f64 = lengths.iter().sum();
810    let row = spacing.along(whole_count(count, reader)?, reader)?;
811    let start = curves
812        .first()
813        .ok_or_else(|| GeopError::new("the path has no curves"))?;
814    let (t0, _) = start.domain();
815    let origin = start.evaluate(t0)?;
816    let mut along = Transport::new(start, t0)?;
817    let mut motions = Vec::new();
818    for k in 1..row.count {
819        // Where to put a copy is a free choice: a sharp distance.
820        let s = row.distance(k)?.to_f64();
821        if s > total {
822            return Err(GeopError::new(format!(
823                "copy {k} would be {s} along a path {total} long: fewer copies, or closer"
824            )));
825        }
826        let (curve, t) = at_length(&curves, &lengths, s)?;
827        let point = curves[curve].evaluate(t)?;
828        let pose = match aligned {
829            false => Pose::new(point.sub(&origin), Quaternion::identity())?,
830            true => {
831                let turn = along.to(&curves, curve, t)?;
832                let to_origin = Pose::new(origin.neg(), Quaternion::identity())?;
833                let there = Pose::new(point, turn)?;
834                there.compose(&to_origin)
835            }
836        };
837        motions.push((k.to_string(), pose));
838    }
839    Ok(motions)
840}
841
842/// The curve of `curves` — each `lengths` long — and the parameter on it
843/// at which `s` of their length is run: the parameter a free choice,
844/// sharp.
845fn at_length(curves: &[NurbCurve3D], lengths: &[f64], s: f64) -> GeopResult<(usize, Scalar)> {
846    let mut run = 0.0;
847    for (k, (curve, &length)) in curves.iter().zip(lengths).enumerate() {
848        let last = k + 1 == curves.len();
849        if s <= run + length || last {
850            let (t0, t1) = curve.domain();
851            let fraction = (s - run) / length;
852            let t = if fraction <= 0.0 {
853                t0
854            } else if fraction >= 1.0 {
855                t1
856            } else {
857                curve.parameter_at_length_fraction(fraction)?
858            };
859            return Ok((k, t));
860        }
861        run += length;
862    }
863    Err(GeopError::new("the path has no curves"))
864}
865
866/// A frame carried along a chain of curves turning as little as it can —
867/// the rotation-minimizing frame, by double reflection (Wang et al.) over
868/// a fixed number of steps per knot span. How a copy is turned along a
869/// curve is the pattern's free choice; this one turns it with the curve,
870/// never about it.
871struct Transport {
872    /// Where it has come to: the curve, the parameter, the point, the
873    /// tangent and the frame's first normal there — plain numbers, a
874    /// chosen orientation, not a measured one.
875    curve: usize,
876    t: f64,
877    point: [f64; 3],
878    tangent: [f64; 3],
879    normal: [f64; 3],
880    /// The frame where it started, the tangent its `x`.
881    first: [[f64; 3]; 3],
882}
883
884/// The steps a [`Transport`] takes per knot span.
885const STEPS_PER_SPAN: usize = 16;
886
887fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
888    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
889}
890
891fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
892    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
893}
894
895fn scaled(a: [f64; 3], s: f64) -> [f64; 3] {
896    [a[0] * s, a[1] * s, a[2] * s]
897}
898
899fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
900    [
901        a[1] * b[2] - a[2] * b[1],
902        a[2] * b[0] - a[0] * b[2],
903        a[0] * b[1] - a[1] * b[0],
904    ]
905}
906
907fn unit(a: [f64; 3]) -> GeopResult<[f64; 3]> {
908    let n = dot(a, a).sqrt();
909    if !(n > 0.0 && n.is_finite()) {
910        return Err(GeopError::new("the path stops: it has no tangent there"));
911    }
912    Ok(scaled(a, 1.0 / n))
913}
914
915/// The point and the unit tangent of `curve` at `t`, plain.
916fn point_tangent(curve: &NurbCurve3D, t: f64) -> GeopResult<([f64; 3], [f64; 3])> {
917    let t = Scalar::from_f64(t);
918    let point = curve.evaluate(t)?.to_f64_array();
919    let tangent = unit(curve.tangent(t)?.to_f64_array())?;
920    Ok((point, tangent))
921}
922
923impl Transport {
924    /// Starting on `curve` at `t`, its first normal the world axis the
925    /// tangent runs least along, made square to it.
926    fn new(curve: &NurbCurve3D, t: Scalar) -> GeopResult<Self> {
927        let (point, tangent) = point_tangent(curve, t.to_f64())?;
928        let axis = (0..3)
929            .min_by(|&a, &b| tangent[a].abs().total_cmp(&tangent[b].abs()))
930            .unwrap_or(0);
931        let mut e = [0.0; 3];
932        e[axis] = 1.0;
933        let normal = unit(sub(e, scaled(tangent, dot(e, tangent))))?;
934        Ok(Self {
935            curve: 0,
936            t: t.to_f64(),
937            point,
938            tangent,
939            normal,
940            first: [tangent, normal, cross(tangent, normal)],
941        })
942    }
943
944    /// One reflection step to `t` on the curve it is on.
945    fn step(&mut self, curve: &NurbCurve3D, t: f64) -> GeopResult<()> {
946        let (point, tangent) = point_tangent(curve, t)?;
947        let v1 = sub(point, self.point);
948        let c1 = dot(v1, v1);
949        let (normal, along) = if c1 > 0.0 {
950            (
951                sub(self.normal, scaled(v1, 2.0 / c1 * dot(v1, self.normal))),
952                sub(self.tangent, scaled(v1, 2.0 / c1 * dot(v1, self.tangent))),
953            )
954        } else {
955            (self.normal, self.tangent)
956        };
957        let v2 = sub(tangent, along);
958        let c2 = dot(v2, v2);
959        let normal = if c2 > 0.0 {
960            sub(normal, scaled(v2, 2.0 / c2 * dot(v2, normal)))
961        } else {
962            normal
963        };
964        // Square to the tangent again, against the rounding of each step.
965        self.normal = unit(sub(normal, scaled(tangent, dot(normal, tangent))))?;
966        (self.point, self.tangent, self.t) = (point, tangent, t);
967        Ok(())
968    }
969
970    /// Carried on to `t` on the curve `to` of `curves` — never back — and
971    /// the turn from where it started to there.
972    fn to(&mut self, curves: &[NurbCurve3D], to: usize, t: Scalar) -> GeopResult<Quaternion> {
973        let t = t.to_f64();
974        while self.curve <= to {
975            let curve = &curves[self.curve];
976            let (_, end) = curve.domain();
977            let stop = if self.curve == to { t } else { end.to_f64() };
978            let spans: Vec<f64> = (curve.breakpoints().iter())
979                .map(|b| b.to_f64())
980                .filter(|&b| b > self.t && b < stop)
981                .chain(std::iter::once(stop))
982                .collect();
983            for b in spans {
984                let a = self.t;
985                for k in 1..=STEPS_PER_SPAN {
986                    let s = a + (b - a) * k as f64 / STEPS_PER_SPAN as f64;
987                    self.step(curve, s)?;
988                }
989            }
990            if self.curve == to {
991                break;
992            }
993            // On to the next curve, where this one ends.
994            self.curve += 1;
995            let (start, _) = curves[self.curve].domain();
996            self.t = start.to_f64();
997            self.step(&curves[self.curve], self.t)?;
998        }
999        let here = [self.tangent, self.normal, cross(self.tangent, self.normal)];
1000        // The rotation taking the first frame to this one: `H Fᵀ`.
1001        let mut columns = [[0.0; 3]; 3];
1002        for (j, column) in columns.iter_mut().enumerate() {
1003            for (i, value) in column.iter_mut().enumerate() {
1004                *value = (0..3).map(|k| here[k][i] * self.first[k][j]).sum();
1005            }
1006        }
1007        Quaternion::from_rotation_columns(
1008            columns.map(|c| Vector3::from_array(c.map(Scalar::from_f64))),
1009        )
1010    }
1011}
1012
1013/// At the points `points`: the seed at the first, or at `from`. Instance
1014/// `k` — the `k`-th point, counting from one — labelled `k`; a point where
1015/// the seed is gets none.
1016fn at_points(
1017    reader: &Reader,
1018    points: &[PointsRef],
1019    from: Option<&PointsRef>,
1020) -> GeopResult<Vec<(String, Pose)>> {
1021    if points.is_empty() {
1022        return Err(GeopError::new("pick the points to pattern at"));
1023    }
1024    let found = resolve_points(reader, points)?;
1025    let (seed, first) = match from {
1026        Some(from) => {
1027            let [(_, at)] = resolve_points(reader, std::slice::from_ref(from))?[..] else {
1028                return Err(GeopError::new(
1029                    "the seed is at one point: pick a point, not a sketch of several",
1030                ));
1031            };
1032            (at, 0)
1033        }
1034        None => (found[0].1, 1),
1035    };
1036    let mut motions = Vec::new();
1037    for (k, (_, at)) in found.iter().enumerate().skip(first) {
1038        if at.could_be_equal(&seed) {
1039            continue;
1040        }
1041        let label = (k + 1 - first).to_string();
1042        motions.push((label, Pose::new(at.sub(&seed), Quaternion::identity())?));
1043    }
1044    Ok(motions)
1045}
1046
1047/// At the offsets `rows`, instance `k` the `k`-th row, counting from one.
1048fn from_table(reader: &Reader, rows: &[Offset]) -> GeopResult<Vec<(String, Pose)>> {
1049    (rows.iter().enumerate())
1050        .map(|(k, row)| {
1051            let ctx = with_context!("row {} of the table", k + 1);
1052            let offset = [&row.x, &row.y, &row.z]
1053                .map(|v| v.resolve(reader).map(Scalar::from_f64))
1054                .into_iter()
1055                .collect::<GeopResult<Vec<_>>>()
1056                .with_context(ctx)?;
1057            let offset = Vector3::from_array([offset[0], offset[1], offset[2]]);
1058            Ok((
1059                (k + 1).to_string(),
1060                Pose::new(offset, Quaternion::identity())?,
1061            ))
1062        })
1063        .collect()
1064}
1065
1066/// At the holes of `features`: the seed in the first, instance `k` in the
1067/// `k`-th from there, counting from one, turned as it is.
1068fn at_holes(reader: &Reader, features: &[FeatureRef]) -> GeopResult<Vec<(String, Pose)>> {
1069    if features.is_empty() {
1070        return Err(GeopError::new(
1071            "pick a hole's wall: the holes it was drilled with are patterned at",
1072        ));
1073    }
1074    let holes = holes_of(reader, features)?;
1075    let first = pose_of(&holes[0].1.seat_frame()?)?.inverse();
1076    (holes.iter().enumerate().skip(1))
1077        .map(|(k, (_, hole))| Ok((k.to_string(), pose_of(&hole.seat_frame()?)?.compose(&first))))
1078        .collect()
1079}
1080
1081/// Filling the flat face `face` on a grid `spacing` apart through where
1082/// the seed is (`anchor`, projected onto the face's plane), every instance
1083/// `margin` or more inside the face's edges: instance `i.j` the `i`-th
1084/// along the plane's `u` and the `j`-th along its `v`.
1085fn fill(
1086    reader: &Reader,
1087    face: Option<&ElementReference<NamedModel, Plane>>,
1088    spacing: &NumericInput,
1089    margin: &NumericInput,
1090    staggered: bool,
1091    anchor: &dyn Fn() -> GeopResult<Vector3>,
1092) -> GeopResult<Vec<(String, Pose)>> {
1093    let face = face.ok_or_else(|| GeopError::new("pick the flat face to fill"))?;
1094    let step = spacing.resolve(reader)?;
1095    if !(step.is_finite() && step > 0.0) {
1096        return Err(GeopError::new(format!(
1097            "a spacing of {step} fills nothing: give one greater than zero"
1098        )));
1099    }
1100    let margin = margin.resolve(reader)?;
1101    if !(margin.is_finite() && margin >= 0.0) {
1102        return Err(GeopError::new(format!(
1103            "a margin of {margin}: give one of zero or more"
1104        )));
1105    }
1106    let instance = face.instance();
1107    let part = match instance.is_empty() {
1108        true => reader.clone(),
1109        false => reader.placed_part(instance)?,
1110    };
1111    let model = read_model(&part)?;
1112    let face_id = model.face_id(face.name())?;
1113    let plane: CoordinateSystem = model.frame_of_face(face_id)?;
1114    let placed = located(reader, instance)?;
1115    let (to_part, to_here) = (placed.inverse().motion(), placed.motion());
1116    // The grid's origin: the seed's anchor, onto the face's plane.
1117    let origin = plane.uv_to_xyz(&plane.to_uv(&to_part.apply(&anchor()?)));
1118    // Its rows, along the plane's `u`, a step apart; staggered, each row
1119    // half a step on from the last, and the rows nearer: a triangle's
1120    // height apart.
1121    let row_step = match staggered {
1122        true => step * 3f64.sqrt() / 2.0,
1123        false => step,
1124    };
1125    let topology = model.topology();
1126    let (mut lo, mut hi) = ([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]);
1127    for edge in topology.face_edges(face_id) {
1128        for cp in &topology.get_edge(edge)?.curve.control_points {
1129            let uv = plane.to_uv(&cp.dehomogenized()?).sub(&plane.to_uv(&origin));
1130            for k in 0..2 {
1131                lo[k] = lo[k].min(uv[k].lower().to_f64());
1132                hi[k] = hi[k].max(uv[k].upper().to_f64());
1133            }
1134        }
1135    }
1136    let range =
1137        |lo: f64, hi: f64, step: f64| (lo / step).floor() as i64..=(hi / step).ceil() as i64;
1138    let rows = range(lo[1], hi[1], row_step);
1139    let columns = range(lo[0] - step, hi[0] + step, step);
1140    let cells = (rows.end() - rows.start() + 1) * (columns.end() - columns.start() + 1);
1141    if cells as usize > 4 * MOST {
1142        return Err(GeopError::new(format!(
1143            "a spacing of {step} would try {cells} places on the face: space them further apart"
1144        )));
1145    }
1146    let surface = &topology.get_face(face_id)?.surface;
1147    let edges: Vec<_> = topology.face_edges(face_id).collect();
1148    let margin = Scalar::from_f64(margin);
1149    let mut motions = Vec::new();
1150    for j in rows {
1151        let shift = match staggered && j.rem_euclid(2) == 1 {
1152            true => step / 2.0,
1153            false => 0.0,
1154        };
1155        for i in columns.clone() {
1156            if (i, j) == (0, 0) {
1157                continue;
1158            }
1159            let (u, v) = (i as f64 * step + shift, j as f64 * row_step);
1160            let offset = plane
1161                .u()
1162                .prod_scalar(Scalar::from_f64(u))
1163                .add(&plane.v().prod_scalar(Scalar::from_f64(v)));
1164            let at = origin.add(&offset);
1165            let Some((su, sv)) =
1166                surface_could_contain(surface, &at, MAX_NODES, min_subdivision_size())?
1167            else {
1168                continue;
1169            };
1170            let inside = face_contains(
1171                topology,
1172                face_id,
1173                su,
1174                sv,
1175                MAX_NODES,
1176                min_subdivision_size(),
1177                FILL_SEED,
1178            )?;
1179            if inside != PointClassification::Inside {
1180                continue;
1181            }
1182            if margin.definitely_greater(Scalar::ZERO) {
1183                let mut clear = true;
1184                for &edge in &edges {
1185                    let curve = topology.get_edge(edge)?.curve.clone();
1186                    let near = closest_points(&DistanceTo::Point(at), &DistanceTo::Curve(curve))?;
1187                    // Kept only where it is surely clear of the edge.
1188                    if !near.distance.lower().definitely_greater(margin)
1189                        && !near.distance.lower().could_be_equal(margin)
1190                    {
1191                        clear = false;
1192                        break;
1193                    }
1194                }
1195                if !clear {
1196                    continue;
1197                }
1198            }
1199            let motion = Pose::new(to_here.rotate(&offset), Quaternion::identity())?;
1200            motions.push((format!("{i}.{j}"), motion));
1201        }
1202    }
1203    Ok(motions)
1204}
1205
1206/// Seeds the ray directions a fill asks its face with.
1207const FILL_SEED: u64 = 0x4649_4C4C_0000_0001;