1use 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
46pub 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#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
57pub struct Direction {
58 pub along: Option<PatternDirection>,
62 #[serde(default)]
64 pub reversed: bool,
65 pub count: NumericInput,
68 pub spacing: Spacing,
70}
71
72impl Direction {
73 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 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 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 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 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
170fn 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#[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#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
195#[serde(tag = "type", rename_all = "snake_case")]
196pub enum Frames {
197 Linear {
199 first: Direction,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
201 second: Option<Direction>,
202 },
203 Circular {
206 axis: Option<TurnAxis>,
207 #[serde(default)]
208 reversed: bool,
209 count: NumericInput,
210 angle: Spacing,
212 },
213 Curve {
217 path: Option<PathRef>,
218 count: NumericInput,
219 spacing: Spacing,
220 #[serde(default)]
221 aligned: bool,
222 },
223 Points {
225 points: Vec<PointsRef>,
226 #[serde(default, skip_serializing_if = "Option::is_none")]
227 from: Option<PointsRef>,
228 },
229 Table { rows: Vec<Offset> },
231 Fill {
235 face: Option<ElementReference<NamedModel, Plane>>,
236 spacing: NumericInput,
237 #[serde(default)]
238 margin: NumericInput,
239 #[serde(default)]
240 staggered: bool,
241 },
242 Holes { features: Vec<FeatureRef> },
245}
246
247const 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
258const MOST: usize = 10_000;
261
262impl Frames {
263 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 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 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 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 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 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 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 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
667fn 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
723fn 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
770fn 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
791fn 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 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
842fn 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
866struct Transport {
872 curve: usize,
876 t: f64,
877 point: [f64; 3],
878 tangent: [f64; 3],
879 normal: [f64; 3],
880 first: [[f64; 3]; 3],
882}
883
884const 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
915fn 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 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 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 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 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 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 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
1013fn 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
1047fn 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
1066fn 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
1081fn 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 let origin = plane.uv_to_xyz(&plane.to_uv(&to_part.apply(&anchor()?)));
1118 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 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
1206const FILL_SEED: u64 = 0x4649_4C4C_0000_0001;