1use 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#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
47pub struct Fillet;
48
49#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
50pub struct FilletArgs {
51 pub edges: Vec<ElementReference<NamedModel, Edge>>,
53 pub radius: NumericInput,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub end_radius: Option<NumericInput>,
58 #[serde(default, skip_serializing_if = "Vec::is_empty")]
60 pub vertex_radii: Vec<VertexRadius>,
61 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub setback: Option<NumericInput>,
66}
67
68#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
70pub struct VertexRadius {
71 pub vertex: ElementReference<NamedModel, Vertex>,
72 pub radius: f64,
73}
74
75impl FilletArgs {
76 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 fn variable(&self) -> bool {
90 self.end_radius.is_some() || !self.vertex_radii.is_empty()
91 }
92
93 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
114fn edge_refs(names: Vec<String>) -> Vec<ElementReference<NamedModel, Edge>> {
116 (names.into_iter())
117 .map(|name| ElementReference::new(model(), name))
118 .collect()
119}
120
121fn 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 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 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 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 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#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
275pub struct Chamfer;
276
277#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
278pub struct ChamferArgs {
279 pub edges: Vec<ElementReference<NamedModel, Edge>>,
281 pub distance: NumericInput,
284 #[serde(default, skip_serializing_if = "Option::is_none")]
286 pub distance2: Option<NumericInput>,
287}
288
289impl ChamferArgs {
290 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 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 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 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}