Skip to main content

geop_ops_pattern/
circular.rs

1//! [`CircularPattern`]: copies of bodies turned around an axis.
2
3use geop_core_editor::{
4    edit_form::{EditForm, FormState},
5    editable::{Context, OperationGroup, OperationInfo, OperationTier},
6    editing::{Number, Unit},
7    part_operation,
8    part_operation::PartOperation,
9};
10use geop_core_math::{
11    geop_error::GeopResult,
12    primitives::{FrameAxis, Motion},
13};
14use geop_core_program::{numeric::NumericInput, part::StepContext, reader::Reader};
15use geop_ops_model::datums::origin_axis;
16use geop_ops_model::{ModelOperation, NamedModel};
17use serde::{Deserialize, Serialize};
18
19use crate::{
20    common::{Naming, Pattern, Seeds, build_pattern, placed_by, run_pattern},
21    references::TurnAxis,
22};
23use geop_ops_model::spacing::{Frames, Spacing, count_field};
24
25/// Copies the bodies `bodies` — solids, and sheets by one of their faces —
26/// or does the features `features` again, turned around an axis: `count` instances, the bodies themselves the
27/// first, each `Spacing::Step` degrees from the last, or spread evenly over
28/// `Spacing::Extent` degrees — over a full turn of 360°, the last a step
29/// short of coming round onto the first again. Turning goes
30/// counter-clockwise looking against the axis, the other way if
31/// `reversed`.
32///
33/// The axis is a line — a straight edge, an axis, a sketch line — or what
34/// a round entity turns around: a circular edge, a cylindrical face.
35///
36/// Copies are held apart or combined — a feature as it combined — and named as
37/// [`crate::LinearPattern`] does, as
38/// `circular_pattern(P)` and `circular_pattern(P,i,X)`.
39#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
40pub struct CircularPattern;
41
42#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
43pub struct CircularPatternArgs {
44    /// The bodies, or the features, to copy, and how to combine copies of
45    /// bodies.
46    #[serde(flatten)]
47    pub seeds: Seeds,
48    /// What to turn around; none, for a step that has not picked it.
49    pub axis: Option<TurnAxis>,
50    /// Turn the other way.
51    #[serde(default)]
52    pub reversed: bool,
53    /// How many instances, the bodies themselves included: a number, or a
54    /// formula of the part's parameters.
55    pub count: NumericInput,
56    /// How far apart they are, in degrees.
57    pub angle: Spacing,
58}
59
60impl Pattern for CircularPatternArgs {
61    const NAMING: Naming = Naming {
62        kind: "circular_pattern",
63        labelled: true,
64    };
65
66    fn seeds(&self) -> &Seeds {
67        &self.seeds
68    }
69
70    fn seed_label(&self) -> &'static str {
71        "0"
72    }
73
74    fn placements(&self, reader: &Reader) -> GeopResult<Vec<(String, Motion)>> {
75        let frames = Frames::Circular {
76            axis: self.axis.clone(),
77            reversed: self.reversed,
78            count: self.count.clone(),
79            angle: self.angle.clone(),
80        };
81        placed_by(&frames, reader, &self.seeds)
82    }
83}
84
85part_operation!(CircularPattern(CircularPatternArgs));
86
87impl PartOperation for CircularPattern {
88    const KIND: &'static str = "circular_pattern";
89    const INFO: OperationInfo = OperationInfo::new(
90        "Circular pattern",
91        "Copy bodies turned around an axis.",
92        OperationGroup::Bodies,
93        OperationTier::Small,
94    )
95    .shortcut("Shift+P")
96    .icon(geop_core_editor::view::icons::CIRCULAR_PATTERN);
97    type Args = CircularPatternArgs;
98    type Session = ();
99
100    /// Four times around the origin's `z` axis, kept as new bodies.
101    fn new_args(_: &Context) -> CircularPatternArgs {
102        CircularPatternArgs {
103            // Nothing picked: what is patterned is the user's choice, a body
104            // or a feature, never guessed.
105            seeds: Seeds::default(),
106            axis: Some(TurnAxis::Datum(origin_axis(FrameAxis::Z))),
107            reversed: false,
108            count: NumericInput::Plain(4.0),
109            angle: Spacing::extent(360.0),
110        }
111    }
112
113    fn run(ctx: &mut StepContext, args: &CircularPatternArgs) -> GeopResult<()> {
114        let _step = ctx.step();
115        run_pattern::<Self>(ctx, args)
116    }
117
118    /// The bodies or the features, and the axis, picked; the count and the
119    /// angles; and how to combine copies of bodies.
120    fn form<'a>(
121        context: &'a Context<'a>,
122        args: &CircularPatternArgs,
123        _: &FormState<()>,
124    ) -> EditForm<'a, CircularPatternArgs> {
125        let before = &context.before;
126        let mut f = EditForm::default();
127        let set_seeds = |args: &mut CircularPatternArgs, seeds| args.seeds = seeds;
128        (args.seeds).fields(&mut f, before, set_seeds, |f| {
129            f.pick_one(
130                "axis",
131                "axis",
132                before,
133                args.axis.as_ref(),
134                |edit, picked| edit.args.axis = Some(picked),
135            )
136            .checkbox("reversed", "reverse direction", args.reversed, |edit, b| {
137                edit.args.reversed = b
138            });
139            count_field(f, "count", &args.count, before, None, |edit, count| {
140                edit.args.count = count
141            });
142            args.angle.fields(
143                f,
144                "angle",
145                ["angle", "total angle"],
146                |label, value| {
147                    Number::numeric(value, label, before, Unit::Angle).range(-360.0, 360.0)
148                },
149                |args, angle| args.angle = angle,
150            );
151        });
152        f
153    }
154}
155
156impl ModelOperation for CircularPattern {
157    fn build(
158        model: &mut NamedModel,
159        reader: &Reader,
160        step: &str,
161        args: &CircularPatternArgs,
162    ) -> GeopResult<()> {
163        build_pattern(model, reader, step, args)
164    }
165}