Skip to main content

geop_ops_extrude_revolve/
loft.rs

1//! [`Loft`]: a body through the profiles of several sketches.
2
3use geop_core_editor::{
4    edit_form::{EditForm, FormState},
5    editable::{Context, OperationGroup, OperationInfo, OperationTier},
6    part_operation,
7    part_operation::PartOperation,
8};
9use geop_core_math::{
10    geop_error::{GeopError, GeopResult, WithContext},
11    with_context,
12};
13use geop_core_program::{element::ElementReference, part::StepContext, reader::Reader};
14use geop_core_sketch::{PointId, Shape};
15use geop_ops_model::sketch::{PlacedSketch, SketchRef, chain_of_pieces};
16use geop_ops_model::{ModelOperation, NamedModel, Namer, Point, build_model, read_model};
17use serde::{Deserialize, Serialize};
18
19use geop_core_math::vector::Vector3;
20use geop_lib_sweep::{Apex, Ends, Section, loft, mark};
21
22use crate::sweeping::{Output, PathRef};
23
24/// Lofts through the profiles of two or more sketches, in order, into a
25/// solid named `loft(L)` for the operation `L` — each sketch's one area, a
26/// single loop — kept as a new body or combined with another solid (see
27/// [`Combine`](geop_ops_model::Combine)). Or, as faces ([`Output::Faces`]), into faces standing on
28/// their own: through the sketches' loops, or through open chains of curves
29/// — between two curves, the ruled surface joining them.
30///
31/// Two profiles are joined by ruled walls, more by walls running smoothly
32/// through them all (see [`geop_lib_sweep::loft`]). A sketch of a single
33/// point, first or last, is a point the loft comes to: the walls run into
34/// it, as a cone into its apex, the vertex `loft(L,K)` for the sketch `K`.
35/// Through its first profile again at the end, it closes on itself: a ring
36/// running smoothly round through three profiles or more, with no caps.
37/// The profiles' corners
38/// — the sketch points where their curves meet at an angle — are lofted
39/// into each other, in order round the loops, so every profile with
40/// corners needs as many: a D and a triangle are refused, naming both. A
41/// profile without corners, a circle, is lined up as a whole. Between two
42/// corners, the curves are cut alike, each where another profile has a
43/// joint at the same fraction of the way along: a piece `X` cut gives
44/// `X#cut1`, `X#cut2`, ..., with the joints `X@cut1`, .... Sketch points on
45/// the profiles' loops picked among the [`LoftArgs::matches`] say instead
46/// which points are lofted into each other: each profile's first with the
47/// others' first, its second with their second, and so on — the curve such
48/// a point lies on split there, its second half named `X#P`.
49///
50/// Or guide curves ([`LoftArgs::guides`]) say which points match: sketches
51/// whose one chain of curves runs from a point of the first profile through
52/// every other to the last. Each guide's point on each profile is matched,
53/// as a joint `K,G` of the profile `K` for the guide `G`, and between
54/// the profiles the walls follow the guides (see [`geop_lib_sweep::loft`]) rather
55/// than run straight.
56///
57/// Named after the first sketch's elements — `X` a piece of a curve and `P`
58/// a joint of the sketch `K` — and the sketches `K`, `M`, ... themselves:
59///
60/// - `loft(L,K,X)`: the wall from `X` — `loft(L,K,X,M>N)` for the one
61///   between the sketches `M` and `N`, when there are more than two;
62/// - `loft(L,K,X,M)`: the curve of the sketch `M` the wall `X` meets there;
63/// - `loft(L,K,P)` (or `loft(L,K,P,M>N)`) / `loft(L,K,P,M)`: the edge from
64///   `P` between two profiles, and its vertex on `M`;
65/// - `loft(L,start)` / `loft(L,end)`: the caps, the first and last profile.
66#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
67pub struct Loft;
68
69#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
70pub struct LoftArgs {
71    /// The sketches to loft through, in order.
72    pub profiles: Vec<SketchRef>,
73    /// Points that correspond across the profiles: sketch points on the
74    /// profiles' loops — corners, or points along curves — each profile's
75    /// in the order picked, the `k`-th of every profile lofted into each
76    /// other.
77    #[serde(default, skip_serializing_if = "Vec::is_empty")]
78    pub matches: Vec<ElementReference<PlacedSketch, Point>>,
79    /// Sketches or 3-D sketches whose curves guide the loft from the first
80    /// profile to the last — at most three — instead of matching points.
81    #[serde(default, skip_serializing_if = "Vec::is_empty")]
82    pub guides: Vec<PathRef>,
83    /// A solid, kept as a new body or combined with another, or faces.
84    #[serde(default)]
85    pub output: Output,
86}
87
88part_operation!(Loft(LoftArgs));
89
90impl PartOperation for Loft {
91    const KIND: &'static str = "loft";
92    const INFO: OperationInfo = OperationInfo::new(
93        "Loft",
94        "Build a solid through the areas of several sketches, or faces through their curves.",
95        OperationGroup::Solid,
96        OperationTier::Small,
97    )
98    .shortcut("L")
99    .icon(crate::icons::LOFT);
100    type Args = LoftArgs;
101    type Session = ();
102
103    /// Through the two newest sketches, joined to the newest solid if there
104    /// is one.
105    fn new_args(context: &Context) -> LoftArgs {
106        let before = &context.before;
107        let sketches = before.targets_of::<PlacedSketch>();
108        LoftArgs {
109            profiles: (sketches
110                .get(sketches.len().saturating_sub(2)..)
111                .unwrap_or_default())
112            .iter()
113            .cloned()
114            .map(SketchRef::whole)
115            .collect(),
116            matches: Vec::new(),
117            guides: Vec::new(),
118            output: Output::new_for(&read_model(before).unwrap_or_default()),
119        }
120    }
121
122    fn run(ctx: &mut StepContext, args: &LoftArgs) -> GeopResult<()> {
123        let _step = ctx.step();
124        build_model::<Self>(ctx, args)
125    }
126    /// The sketches, picked in order; the points matched on them, or the
127    /// guide curves; whether a face; and how to combine.
128    fn form<'a>(
129        context: &'a Context<'a>,
130        args: &LoftArgs,
131        _: &FormState<()>,
132    ) -> EditForm<'a, LoftArgs> {
133        let before = &context.before;
134        let mut f = EditForm::<LoftArgs>::default();
135        f.pick(
136            "profiles",
137            "profiles",
138            before,
139            &args.profiles,
140            true,
141            |edit, profiles| edit.args.profiles = profiles,
142        )
143        .pick(
144            "matches",
145            "matching points",
146            before,
147            &args.matches,
148            true,
149            |edit, matches| edit.args.matches = matches,
150        )
151        .pick(
152            "guides",
153            "guide curves",
154            before,
155            &args.guides,
156            true,
157            |edit, guides| edit.args.guides = guides,
158        );
159        f.form.dialog.optional("matches");
160        f.form.dialog.optional("guides");
161        let output = |args: &mut LoftArgs, output| args.output = output;
162        (args.output).fields(&mut f, before, false, output);
163        f
164    }
165}
166
167impl ModelOperation for Loft {
168    fn build(
169        model: &mut NamedModel,
170        reader: &Reader,
171        step: &str,
172        args: &LoftArgs,
173    ) -> GeopResult<()> {
174        let namer = Namer::new("loft", step)?;
175        // Through the first profile again at the end, the loft closes on
176        // itself. A sketch of a single point, first or last, is a point the
177        // loft
178        // comes to.
179        let profiles = match args.profiles.as_slice() {
180            [first, .., again] if args.profiles.len() > 1 && first == again => {
181                &args.profiles[..args.profiles.len() - 1]
182            }
183            all => all,
184        };
185        let closed = profiles.len() < args.profiles.len();
186        let last = profiles.len().saturating_sub(1);
187        let mut apexes = [None, None];
188        let mut sections = Vec::new();
189        for (k, profile) in profiles.iter().enumerate() {
190            let placed = reader.retrieve_element(profile)?;
191            if let Some(point) = single_point(placed)? {
192                let end = match k {
193                    0 => 0,
194                    k if k == last => 1,
195                    _ => {
196                        return Err(GeopError::new(format!(
197                            "loft: the sketch {:?} is a point, between profiles: a loft comes to a point only at its start or end",
198                            placed.name
199                        )));
200                    }
201                };
202                apexes[end] = Some(Apex {
203                    point,
204                    name: placed.name.to_string(),
205                });
206                continue;
207            }
208            sections.push(section(
209                reader,
210                profile,
211                &args.output,
212                &matched_points(args, profile)?,
213            )?);
214        }
215        let ends = match (closed, &apexes) {
216            (false, _) => Ends::Open(apexes),
217            (true, [None, None]) => Ends::Closed,
218            (true, _) => {
219                return Err(GeopError::new(
220                    "loft: a loft closing on itself has no end to come to a point at",
221                ));
222            }
223        };
224        let guides = (args.guides.iter())
225            .map(|guide| {
226                if args.profiles.iter().any(|profile| guide.is_sketch(profile)) {
227                    return Err(GeopError::new(format!(
228                        "loft: the guide {guide:?} is one of the profiles: a guide is a curve of its own"
229                    )));
230                }
231                guide.resolve(reader)
232            })
233            .collect::<GeopResult<Vec<_>>>()?;
234        (args.output).build_whole(model, &namer, step, |model, name| {
235            loft(model, &namer, name, &sections, &ends, &guides)
236        })
237    }
238}
239
240/// The points of the profile `profile` among `args.matches`, in the order
241/// picked: an error for a point of a sketch that is no profile.
242fn matched_points(args: &LoftArgs, profile: &SketchRef) -> GeopResult<Vec<PointId>> {
243    let mut found = Vec::new();
244    for point in &args.matches {
245        let sketch = point.target();
246        if !args.profiles.iter().any(|p| p.target() == sketch) {
247            return Err(GeopError::new(format!(
248                "loft: the matching point {} is of the sketch {:?}, which is no profile",
249                point.name(),
250                sketch.name()
251            )));
252        }
253        if sketch == profile.target() {
254            found.push(point.name().parse()?);
255        }
256    }
257    Ok(found)
258}
259
260/// Where the sketch `placed` is a single point and draws nothing else, the
261/// point in space; none for a sketch that draws curves.
262fn single_point(placed: &PlacedSketch) -> GeopResult<Option<Vector3>> {
263    let sketch = &placed.sketch;
264    if sketch.curves.values().any(|c| !c.construction) || sketch.points.len() != 1 {
265        return Ok(None);
266    }
267    let geometry = sketch.enclose()?;
268    Ok(geometry
269        .points
270        .values()
271        .next()
272        .map(|at| placed.plane.uv_to_xyz(at)))
273}
274
275/// The sketch `profile`, as `reader` reads it, as a section: its one loop
276/// — or, lofted into faces, its one open chain if it encloses nothing (see
277/// [`Output::shape`]) — with a joint at each of its points `matched`.
278fn section(
279    reader: &Reader,
280    profile: &SketchRef,
281    output: &Output,
282    matched: &[PointId],
283) -> GeopResult<Section> {
284    let ctx = with_context!("profile {:?}", profile.target().name());
285    let placed = reader.retrieve_element(profile).with_context(ctx)?;
286    let name = &placed.name;
287    let sketch = &placed.sketch;
288    let geometry = sketch.enclose().with_context(ctx)?;
289    let shape = output.shape(sketch).with_context(ctx)?;
290    let (lp, closed) = match shape {
291        Shape::Regions(mut regions) if regions.len() == 1 && regions[0].holes.is_empty() => {
292            (regions.remove(0).outer, true)
293        }
294        Shape::Regions(_) => {
295            return Err(GeopError::new(format!(
296                "loft: the profile {name:?} has holes or several areas, but a loft runs through single loops"
297            )));
298        }
299        Shape::Chain(chain) => (chain, false),
300    };
301    let mut profile = chain_of_pieces(
302        name,
303        lp.to_nurbs(sketch, &geometry).with_context(ctx)?,
304        closed,
305    );
306    let mut joints = Vec::new();
307    for &point in matched {
308        let at = geometry.points.get(&point).ok_or_else(|| {
309            GeopError::new(format!(
310                "loft: the matching point {point} is no point of the sketch"
311            ))
312        })?;
313        let (marked, joint) = mark(&profile, at, &format!("{name},{point}"))
314            .with_context(with_context!("the matching point {point}"))
315            .with_context(ctx)?;
316        profile = marked;
317        joints.push(joint);
318    }
319    Ok(Section {
320        plane: placed.plane.clone(),
321        profile,
322        name: name.to_string(),
323        matched: joints,
324    })
325}