Skip to main content

geop_ops_fillet/
operation.rs

1//! [`Fillet`]: round a solid's edges. [`Chamfer`]: bevel them. Both blend
2//! the edges picked (see [`crate::blend`]).
3
4use geop_core_editor::view::icons;
5use geop_core_editor::{
6    edit_form::{EditForm, FormState},
7    editable::{Context, OperationGroup, OperationInfo, OperationTier},
8    editing::{Number, Unit},
9    part_operation,
10    part_operation::PartOperation,
11};
12use geop_core_math::geop_error::{GeopResult, WithContext};
13use geop_core_program::{
14    element::ElementReference, numeric::NumericInput, part::StepContext, reader::Reader,
15};
16use geop_ops_model::{Edge, ModelOperation, NamedModel, Namer, Vertex, build_model, model};
17use serde::{Deserialize, Serialize};
18
19use crate::{
20    blend::{BlendShape, blend},
21    rolling::Radii,
22};
23
24/// Rounds the edges named `edges` of a solid with a fillet of `radius`, for
25/// the operation `F`: the solid is consumed and the result named
26/// `fillet(F)`. Every face, edge and vertex that survives keeps its name;
27/// the round face of edge `E` is `fillet(F,E,fillet)` — by span,
28/// `fillet(F,E,fillet,s0)`, ..., for one skinned in several (see
29/// [`crate::rolling`]) — and what applying it creates is named as a
30/// boolean names it, `fillet(F,E,...)` (see [`crate::blend::blend`]). A
31/// convex edge is cut round, a concave one filled in.
32///
33/// Any edge is rounded, by a ball rolled along it, which also takes its
34/// tangent chain along: exactly where the ball's section is carried along
35/// the edge unchanged, a straight one between two planes or a circle
36/// around its faces' axis. With `end_radius` the
37/// radius changes linearly along each chain, from `radius` where its first
38/// picked edge starts to `end_radius` where the chain ends; `vertex_radii`
39/// set it at vertices along a chain, linearly in between. A corner every
40/// edge of which is rounded, with one radius, is rounded by the ball
41/// touching its three faces, `fillet(F,V,corner)` for its vertex `V` (see
42/// [`crate::corner`]); with `setback`, or where no one ball touches its
43/// faces, it is set back instead: each fillet ends that far from the vertex
44/// and a smooth patch closes the corner, `fillet(F,V,corner,k)` along each
45/// side of its loop and `fillet(F,V,corner)` inside.
46#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
47pub struct Fillet;
48
49#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
50pub struct FilletArgs {
51    /// The edges to round, all of one solid.
52    pub edges: Vec<ElementReference<NamedModel, Edge>>,
53    /// The radius: a number, or a formula of the part's parameters.
54    pub radius: NumericInput,
55    /// The radius at the end of every chain, if it changes along it.
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub end_radius: Option<NumericInput>,
58    /// Radii at vertices along the chains.
59    #[serde(default, skip_serializing_if = "Vec::is_empty")]
60    pub vertex_radii: Vec<VertexRadius>,
61    /// How far from the vertex every fillet ending at a corner every edge
62    /// of which is rounded ends, the corner closed by a smooth patch
63    /// between them, if it is set back.
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub setback: Option<NumericInput>,
66}
67
68/// The radius a fillet has at a vertex, by name.
69#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
70pub struct VertexRadius {
71    pub vertex: ElementReference<NamedModel, Vertex>,
72    pub radius: f64,
73}
74
75impl FilletArgs {
76    /// Round the edges named `edges` with `radius` — a number, or a
77    /// formula's text — the same all along.
78    pub fn constant(edges: Vec<String>, radius: impl Into<NumericInput>) -> Self {
79        FilletArgs {
80            edges: edge_refs(edges),
81            radius: radius.into(),
82            end_radius: None,
83            vertex_radii: Vec::new(),
84            setback: None,
85        }
86    }
87
88    /// Whether the radius changes along the edges.
89    fn variable(&self) -> bool {
90        self.end_radius.is_some() || !self.vertex_radii.is_empty()
91    }
92
93    /// The radii, as `reader` reads the parameters.
94    fn radii(&self, reader: &Reader) -> GeopResult<Radii> {
95        Ok(Radii {
96            radius: self.radius.resolve(reader).concerning("radius")?,
97            end_radius: match &self.end_radius {
98                Some(r) => Some(r.resolve(reader)?),
99                None => None,
100            },
101            at_vertices: self
102                .vertex_radii
103                .iter()
104                .map(|v| (v.vertex.name().to_string(), v.radius))
105                .collect(),
106            setback: match &self.setback {
107                Some(d) => Some(d.resolve(reader)?),
108                None => None,
109            },
110        })
111    }
112}
113
114/// The edges named `names`.
115fn edge_refs(names: Vec<String>) -> Vec<ElementReference<NamedModel, Edge>> {
116    (names.into_iter())
117        .map(|name| ElementReference::new(model(), name))
118        .collect()
119}
120
121/// The names of `edges`, as [`blend`] takes them.
122fn edge_names(edges: &[ElementReference<NamedModel, Edge>]) -> Vec<String> {
123    edges.iter().map(|e| e.name().to_string()).collect()
124}
125
126part_operation!(Fillet(FilletArgs), Chamfer(ChamferArgs));
127
128impl PartOperation for Fillet {
129    const KIND: &'static str = "fillet";
130    const INFO: OperationInfo = OperationInfo::new(
131        "Fillet",
132        "Round a solid's edges.",
133        OperationGroup::Features,
134        OperationTier::Big,
135    )
136    .shortcut("Shift+F")
137    .icon(icons::FILLET);
138    type Args = FilletArgs;
139    type Session = ();
140
141    /// No edges yet, and a radius of one.
142    fn new_args(_: &Context) -> FilletArgs {
143        FilletArgs::constant(Vec::new(), 1.0)
144    }
145
146    fn run(ctx: &mut StepContext, args: &FilletArgs) -> GeopResult<()> {
147        let _step = ctx.step();
148        build_model::<Self>(ctx, args)
149    }
150
151    /// The edges, picked, and the radius; whether corners are set back,
152    /// and how far — and, varying, the radius at the end and at vertices
153    /// picked, each a number of its own.
154    fn form<'a>(
155        context: &'a Context<'a>,
156        args: &FilletArgs,
157        _: &FormState<()>,
158    ) -> EditForm<'a, FilletArgs> {
159        let before = &context.before;
160        let mut f = EditForm::<FilletArgs>::default();
161        f.pick(
162            "edges",
163            "edges",
164            before,
165            &args.edges,
166            true,
167            |edit, edges| edit.args.edges = edges,
168        );
169        let radius = Number::numeric(&args.radius, "radius", before, Unit::Length);
170        f.numeric("radius", radius.range(0.0, 1.0), |edit, radius| {
171            edit.args.radius = radius
172        })
173        .checkbox(
174            "set_back",
175            "set back corners",
176            args.setback.is_some(),
177            move |edit, on| {
178                // Set back twice the radius, to start with.
179                let radius = edit.args.radius.resolve(before).unwrap_or(0.1);
180                edit.args.setback = on.then(|| (2.0 * radius).into());
181            },
182        );
183        if let Some(setback) = &args.setback {
184            let setback = Number::numeric(setback, "setback", before, Unit::Length);
185            f.numeric("setback", setback.range(0.0, 1.0), |edit, d| {
186                edit.args.setback = Some(d)
187            });
188        }
189        f.checkbox(
190            "variable",
191            "variable radius",
192            args.variable(),
193            |edit, on| {
194                let args = edit.args;
195                args.end_radius = on.then(|| args.radius.clone());
196                if !on {
197                    args.vertex_radii.clear();
198                }
199            },
200        );
201        if !args.variable() {
202            return f;
203        }
204        let end = args.end_radius.as_ref().unwrap_or(&args.radius);
205        let end = Number::numeric(end, "end radius", before, Unit::Length);
206        f.numeric("end_radius", end.range(0.0, 1.0), |edit, radius| {
207            edit.args.end_radius = Some(radius)
208        });
209        let vertices: Vec<_> = (args.vertex_radii.iter())
210            .map(|v| v.vertex.clone())
211            .collect();
212        f.pick(
213            "radius_vertices",
214            "radii at vertices",
215            before,
216            &vertices,
217            true,
218            |edit, picked| {
219                // A new vertex starts at the radius as it is now.
220                let args = edit.args;
221                let radius = args.radius.resolve(before).unwrap_or(0.1);
222                let old = std::mem::take(&mut args.vertex_radii);
223                args.vertex_radii = (picked.into_iter())
224                    .map(|vertex| VertexRadius {
225                        radius: (old.iter().find(|v| v.vertex == vertex))
226                            .map_or(radius, |v| v.radius),
227                        vertex,
228                    })
229                    .collect();
230            },
231        );
232        f.form.dialog.optional("radius_vertices");
233        for (i, v) in args.vertex_radii.iter().enumerate() {
234            let label = format!("radius at {}", v.vertex.name());
235            let number = Number::new(label, v.radius, Unit::Length).range(0.0, 1.0);
236            f.number(
237                &format!("vertex_radius_{i}"),
238                number,
239                move |edit, radius| {
240                    if let Some(v) = edit.args.vertex_radii.get_mut(i) {
241                        v.radius = radius;
242                    }
243                },
244            );
245        }
246        f
247    }
248}
249
250impl ModelOperation for Fillet {
251    fn build(
252        model: &mut NamedModel,
253        reader: &Reader,
254        step: &str,
255        args: &FilletArgs,
256    ) -> GeopResult<()> {
257        let namer = Namer::new("fillet", step)?;
258        let radii = args.radii(reader)?;
259        let shape = BlendShape::Fillet { radii };
260        blend(model, &namer, &edge_names(&args.edges), &shape)?;
261        Ok(())
262    }
263}
264
265/// Bevels the edges named `edges` of a solid with a chamfer `distance` into
266/// both faces — or, with `distance2`, `distance` into the face on the left
267/// of each edge as it runs (seen from outside the solid) and `distance2`
268/// into the one on its right — for the operation `C`: the solid is consumed
269/// and the result named `chamfer(C)`, the bevel face of edge `E`
270/// `chamfer(C,E,chamfer)`, named like a [`Fillet`]'s otherwise. Any edge is
271/// bevelled: a straight one between two planes and a circle around its
272/// faces' axis exactly, every other one along its tangent chain by chords
273/// the distances into the faces, square to the edge.
274#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
275pub struct Chamfer;
276
277#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
278pub struct ChamferArgs {
279    /// The edges to bevel, all of one solid.
280    pub edges: Vec<ElementReference<NamedModel, Edge>>,
281    /// How far into the faces: a number, or a formula of the part's
282    /// parameters.
283    pub distance: NumericInput,
284    /// The distance into the face on each edge's right, if it differs.
285    #[serde(default, skip_serializing_if = "Option::is_none")]
286    pub distance2: Option<NumericInput>,
287}
288
289impl ChamferArgs {
290    /// Bevel the edges named `edges` by `distance` — a number, or a
291    /// formula's text — into both faces.
292    pub fn even(edges: Vec<String>, distance: impl Into<NumericInput>) -> Self {
293        ChamferArgs {
294            edges: edge_refs(edges),
295            distance: distance.into(),
296            distance2: None,
297        }
298    }
299
300    /// Into the face on the left and on the right of each edge, as
301    /// `reader` reads the parameters.
302    fn distances(&self, reader: &Reader) -> GeopResult<[f64; 2]> {
303        let left = self.distance.resolve(reader)?;
304        let right = match &self.distance2 {
305            Some(d) => d.resolve(reader)?,
306            None => left,
307        };
308        Ok([left, right])
309    }
310}
311
312impl PartOperation for Chamfer {
313    const KIND: &'static str = "chamfer";
314    const INFO: OperationInfo = OperationInfo::new(
315        "Chamfer",
316        "Bevel a solid's edges.",
317        OperationGroup::Features,
318        OperationTier::Small,
319    )
320    .shortcut("C")
321    .icon(icons::CHAMFER);
322    type Args = ChamferArgs;
323    type Session = ();
324
325    /// No edges yet, and a small distance, the same into both faces.
326    fn new_args(_: &Context) -> ChamferArgs {
327        ChamferArgs::even(Vec::new(), 0.1)
328    }
329
330    fn run(ctx: &mut StepContext, args: &ChamferArgs) -> GeopResult<()> {
331        let _step = ctx.step();
332        build_model::<Self>(ctx, args)
333    }
334
335    /// The edges, picked, the distance, and whether the second face gets a
336    /// distance of its own.
337    fn form<'a>(
338        context: &'a Context<'a>,
339        args: &ChamferArgs,
340        _: &FormState<()>,
341    ) -> EditForm<'a, ChamferArgs> {
342        let before = &context.before;
343        let mut f = EditForm::<ChamferArgs>::default();
344        f.pick(
345            "edges",
346            "edges",
347            before,
348            &args.edges,
349            true,
350            |edit, edges| edit.args.edges = edges,
351        );
352        let distance = Number::numeric(&args.distance, "distance", before, Unit::Length);
353        f.numeric("distance", distance.range(0.0, 1.0), |edit, distance| {
354            edit.args.distance = distance
355        });
356        let two = args.distance2.is_some();
357        f.checkbox("two_distances", "two distances", two, |edit, on| {
358            edit.args.distance2 = on.then(|| edit.args.distance.clone());
359        });
360        if let Some(distance2) = &args.distance2 {
361            let distance2 = Number::numeric(distance2, "distance 2", before, Unit::Length);
362            f.numeric("distance2", distance2.range(0.0, 1.0), |edit, distance| {
363                edit.args.distance2 = Some(distance)
364            });
365        }
366        f
367    }
368}
369
370impl ModelOperation for Chamfer {
371    fn build(
372        model: &mut NamedModel,
373        reader: &Reader,
374        step: &str,
375        args: &ChamferArgs,
376    ) -> GeopResult<()> {
377        let namer = Namer::new("chamfer", step)?;
378        let distances = args.distances(reader)?;
379        let shape = BlendShape::Chamfer { distances };
380        blend(model, &namer, &edge_names(&args.edges), &shape)?;
381        Ok(())
382    }
383}