Skip to main content

geop_ops_extrude_revolve/
sweep.rs

1//! [`Sweep`]: carry a sketch's area along the curves of another sketch, or
2//! of a 3-D sketch.
3
4use geop_core_editor::{
5    edit_form::{EditForm, FormState},
6    editable::{Context, OperationGroup, OperationInfo, OperationTier},
7    editing::{Choice, Number, Unit},
8    part_operation,
9    part_operation::PartOperation,
10};
11use geop_core_math::geop_error::{GeopError, GeopResult, WithContext};
12use geop_core_program::{element::ElementReference, part::StepContext, reader::Reader};
13use geop_ops_model::sketch::{PlacedSketch, SketchRef, shape_profiles};
14use geop_ops_model::sketch3d::PlacedSketch3d;
15use geop_ops_model::{ModelOperation, NamedModel, Namer, build_model, read_model};
16use serde::{Deserialize, Serialize};
17
18use geop_lib_sweep::{
19    Control, Orientation, Rebuild, SweepLoop, profile_centre, starting_near, sweep_along,
20};
21
22use crate::sweeping::{Output, PathRef};
23
24/// Sweeps the areas of a sketch — the profile — along the curves of
25/// another sketch or of a 3-D sketch — the path — into a solid named `sweep(W)` for the
26/// operation `W`, kept as a new body or combined with another solid (see
27/// [`Combine`](geop_ops_model::Combine)). Or, as faces ([`Output::Faces`]), sweeps the profile's
28/// curves into faces standing on their own — the area's outline, or a chain
29/// of curves enclosing nothing.
30///
31/// The path is the path sketch's one chain of curves, or its one loop, which
32/// sweeps a ring, or an edge of the part — a 3-D sketch's chain runs
33/// through space, and the profile turns with it as little as it can. The
34/// profile travels along it from where it is drawn, square to the path as
35/// it was there (see [`geop_lib_sweep::sweep_along`]): draw it
36/// at the start of the path, its plane across it. An open path starts at
37/// its end nearer the profile, a closed one at its joint nearest the
38/// profile. The path is tangent-continuous, except where two lines meet at
39/// an angle, which the sweep mitres.
40///
41/// Named after the profile's elements — `X` a piece of a curve and `P` a
42/// joint of the profile sketch `K`, as for [`crate::Extrude`] — and the
43/// path's — `C` a piece of a curve and `J` a joint of the path sketch `L`:
44///
45/// - `sweep(W,K,X,L,C)`: the face `X` sweeps along `C`;
46/// - `sweep(W,K,X,L,J)`: `X` where the path passes `J`;
47/// - `sweep(W,K,P,L,C)` / `sweep(W,K,P,L,J)`: the edge `P` sweeps along `C`,
48///   and its vertex at `J`;
49/// - `sweep(W,start)` / `sweep(W,end)`: the caps of an open path.
50///
51/// Along an open path the profile can change as it goes (see
52/// [`Control`]): twisted and scaled evenly along the path, or shaped by one
53/// or two guide rails — sketches whose one chain of curves starts on the
54/// profile's plane, at a point of the profile, and runs along beside the
55/// path: that point of the profile follows the rail, the whole profile
56/// turned and scaled with it. And it can keep facing the way it is drawn
57/// rather than turn with the path ([`Orientation::FixedNormal`]).
58#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
59pub struct Sweep;
60
61#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
62pub struct SweepArgs {
63    /// The sketch to sweep.
64    pub profile: SketchRef,
65    /// What it is swept along: a sketch's or a 3-D sketch's one chain of
66    /// curves, or one loop, or an edge.
67    pub path: Option<PathRef>,
68    /// Whether the profile turns with the path or keeps facing one way.
69    #[serde(default)]
70    pub orientation: Orientation,
71    /// How far the profile turns about the path from start to end, in
72    /// degrees, counter-clockwise in its sketch.
73    #[serde(default, skip_serializing_if = "is_zero")]
74    pub twist: f64,
75    /// The profile's size at the end, as a multiple of its size where drawn.
76    #[serde(default = "one", skip_serializing_if = "is_one")]
77    pub end_scale: f64,
78    /// What guides the profile — one or two, each a path as the sweep's
79    /// own is.
80    #[serde(default, skip_serializing_if = "Vec::is_empty")]
81    pub rails: Vec<PathRef>,
82    /// A solid, kept as a new body or combined with another, or faces.
83    #[serde(default)]
84    pub output: Output,
85}
86
87fn is_zero(x: &f64) -> bool {
88    *x == 0.0
89}
90
91fn one() -> f64 {
92    1.0
93}
94
95fn is_one(x: &f64) -> bool {
96    *x == 1.0
97}
98
99/// The orientations a sweep offers, by value and label.
100const ORIENTATIONS: [(Orientation, &str, &str); 2] = [
101    (Orientation::FollowPath, "follow_path", "follow path"),
102    (
103        Orientation::FixedNormal,
104        "fixed_normal",
105        "keep normal fixed",
106    ),
107];
108
109part_operation!(Sweep(SweepArgs));
110
111impl PartOperation for Sweep {
112    const KIND: &'static str = "sweep";
113    const INFO: OperationInfo = OperationInfo::new(
114        "Sweep",
115        "Sweep a sketch's area along the curves of another sketch into a solid, or its curves into faces.",
116        OperationGroup::Solid,
117        OperationTier::Small,
118    )
119    .shortcut("W")
120    .icon(crate::icons::SWEEP);
121    type Args = SweepArgs;
122    type Session = ();
123
124    /// The newest sketch along the sketch before it — joined to the newest
125    /// solid if there is one.
126    fn new_args(context: &Context) -> SweepArgs {
127        let before = &context.before;
128        let mut sketches = before.targets_of::<PlacedSketch>();
129        // The newest 3-D sketch, which is drawn to be swept along; else
130        // the newest sketch.
131        let path = match before.targets_of::<PlacedSketch3d>().pop() {
132            Some(sketch) => Some(PathRef::Sketch3d(ElementReference::whole(sketch))),
133            None => (sketches.pop()).map(|sketch| PathRef::Sketch(ElementReference::whole(sketch))),
134        };
135        let profile = sketches
136            .pop()
137            .unwrap_or_else(|| geop_ops_model::sketch::sketch(""));
138        SweepArgs {
139            profile: SketchRef::whole(profile),
140            path,
141            orientation: Orientation::FollowPath,
142            twist: 0.0,
143            end_scale: 1.0,
144            rails: Vec::new(),
145            output: Output::new_for(&read_model(before).unwrap_or_default()),
146        }
147    }
148
149    fn run(ctx: &mut StepContext, args: &SweepArgs) -> GeopResult<()> {
150        let _step = ctx.step();
151        build_model::<Self>(ctx, args)
152    }
153    /// The profile and the path, picked; the rails, if any; how the profile
154    /// turns, twists and scales; whether a face; and how to combine.
155    fn form<'a>(
156        context: &'a Context<'a>,
157        args: &SweepArgs,
158        _: &FormState<()>,
159    ) -> EditForm<'a, SweepArgs> {
160        let before = &context.before;
161        let mut f = EditForm::<SweepArgs>::default();
162        f.pick_one(
163            "profile",
164            "profile",
165            before,
166            Some(&args.profile),
167            |edit, profile| edit.args.profile = profile,
168        )
169        .pick_one("path", "path", before, args.path.as_ref(), |edit, path| {
170            edit.args.path = Some(path)
171        })
172        .pick(
173            "rails",
174            "guide rails",
175            before,
176            &args.rails,
177            true,
178            |edit, rails| edit.args.rails = rails,
179        );
180        f.form.dialog.optional("rails");
181        let orientation =
182            (ORIENTATIONS.iter().find(|o| o.0 == args.orientation)).map_or("", |o| o.1);
183        let orientations = ORIENTATIONS
184            .iter()
185            .map(|&(_, value, label)| Choice::new(value, label));
186        let label = "orientation";
187        f.select(
188            label,
189            label,
190            orientation,
191            orientations.collect(),
192            false,
193            |edit, choice| {
194                if let Some(&(orientation, ..)) = ORIENTATIONS.iter().find(|o| o.1 == choice) {
195                    edit.args.orientation = orientation;
196                }
197            },
198        );
199        if args.rails.is_empty() {
200            let twist = Number::new("twist", args.twist, Unit::Angle);
201            f.number("twist", twist, |edit, v| edit.args.twist = v);
202            let end_scale =
203                Number::new("end scale", args.end_scale, Unit::Fraction).range(0.0, 4.0);
204            f.number("end_scale", end_scale, |edit, v| edit.args.end_scale = v);
205        }
206        let output = |args: &mut SweepArgs, output| args.output = output;
207        (args.output).fields(&mut f, before, false, output);
208        f
209    }
210}
211
212impl ModelOperation for Sweep {
213    fn build(
214        model: &mut NamedModel,
215        reader: &Reader,
216        step: &str,
217        args: &SweepArgs,
218    ) -> GeopResult<()> {
219        let namer = Namer::new("sweep", step)?;
220        let path = (args.path.as_ref())
221            .ok_or_else(|| GeopError::new("sweep: pick the path to sweep along"))?;
222        if path.is_sketch(&args.profile) {
223            return Err(GeopError::new(
224                "sweep: the profile and the path are two different sketches",
225            ));
226        }
227        let placed = reader
228            .retrieve_element(&args.profile)
229            .concerning("profile")?;
230        let sketch = &placed.sketch;
231        let geometry = sketch.enclose()?;
232        let shape = args.output.shape(sketch)?;
233        let loops: Vec<_> = (shape_profiles(&placed.name, sketch, &geometry, shape)?.into_iter())
234            .map(SweepLoop::plain)
235            .collect();
236        let plane = &placed.plane;
237        let chain = path.resolve(reader)?;
238        let chain = starting_near(chain, &profile_centre(plane, &loops)?)?;
239        let mut rails = Vec::new();
240        for rail in &args.rails {
241            if rail.is_sketch(&args.profile) || rail == path {
242                return Err(GeopError::new(format!(
243                    "sweep: the rail {rail:?} is the profile or the path: a rail is a curve of its own"
244                )));
245            }
246            rails.push(rail.resolve(reader)?);
247        }
248        let control = Control {
249            orientation: args.orientation,
250            twist: args.twist.to_radians(),
251            end_scale: args.end_scale,
252            rails,
253        };
254
255        (args.output).build_whole(model, &namer, step, |model, name| {
256            sweep_along(
257                model,
258                &namer,
259                name,
260                &chain,
261                plane,
262                &loops,
263                &control,
264                Some(Rebuild {
265                    room: geop_lib_shell::room,
266                    rebuilt: geop_lib_shell::rebuilt,
267                }),
268            )
269        })
270    }
271}