1use geop_core_editor::{
9 edit_form::{Edit, EditForm, FormState, choices, chosen},
10 editable::{Context, OperationGroup, OperationInfo, OperationTier},
11 editing::{CanvasEvent, Change, Gizmo, Number, Unit},
12 part_operation,
13 part_operation::PartOperation,
14};
15use geop_core_geometry::{nurb_surface::NurbSurface3D, search::PROJECT_ITERATIONS};
16use geop_core_math::{
17 geop_error::{GeopError, GeopResult},
18 primitives::Motion,
19 scalars::Scalar,
20 vector::Vector3,
21};
22use geop_core_program::{element::ElementReference, part::StepContext, reader::Reader};
23use geop_core_topology::FaceId;
24use geop_lib_shell::{Heal, Smooth, delete_faces, replace_faces};
25use geop_ops_model::{
26 Body, Face, ModelOperation, NamedModel, Namer, build_model, model, read_model, resolve_bodies,
27};
28use serde::{Deserialize, Serialize};
29
30type FaceRef = ElementReference<NamedModel, Face>;
32
33fn face_ids(model: &NamedModel, faces: &[FaceRef]) -> GeopResult<Vec<FaceId>> {
35 faces.iter().map(|f| model.face_id(f.name())).collect()
36}
37
38fn finite(what: &str, value: f64) -> GeopResult<()> {
40 if value.is_finite() {
41 Ok(())
42 } else {
43 Err(GeopError::new(format!("the {what} is not a number")))
44 }
45}
46
47fn faces_middle(reader: &Reader, faces: &[FaceRef]) -> Option<Vector3> {
50 let part = read_model(reader).ok()?;
51 let model = part.topology();
52 let mut sum = Vector3::zero();
53 let mut count = 0usize;
54 for face in face_ids(&part, faces).ok()? {
55 for edge in model.face_edges(face) {
56 let start = model.get_edge(edge).ok()?.start_vertex;
57 sum = sum.add(&model.get_vertex(start).ok()?.point);
58 count += 1;
59 }
60 }
61 let share = Scalar::from_ratio(1, i64::try_from(count).ok()?).ok()?;
62 Some(sum.prod_scalar(share))
63}
64
65#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
74pub struct MoveFace;
75
76#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
77pub struct MoveFaceArgs {
78 pub faces: Vec<FaceRef>,
80 #[serde(default)]
82 pub translation: [f64; 3],
83}
84
85#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
94pub struct OffsetFace;
95
96#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
97pub struct OffsetFaceArgs {
98 pub faces: Vec<FaceRef>,
100 pub distance: f64,
102}
103
104#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
110pub struct ReplaceFace;
111
112#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
113pub struct ReplaceFaceArgs {
114 pub face: FaceRef,
116 pub with: FaceRef,
118}
119
120#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
128pub struct DeleteFace;
129
130#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
131pub struct DeleteFaceArgs {
132 pub faces: Vec<FaceRef>,
134 #[serde(default)]
136 pub heal: HealGap,
137 #[serde(default)]
141 pub whole_feature: bool,
142}
143
144#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
146#[serde(rename_all = "snake_case")]
147pub enum HealGap {
148 #[default]
150 Heal,
151 Open,
153}
154
155impl HealGap {
156 const ALL: [(HealGap, &'static str, &'static str); 2] = [
157 (HealGap::Heal, "heal", "Heal the gap"),
158 (HealGap::Open, "open", "Leave it open"),
159 ];
160}
161
162#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
170pub struct MergeFaces;
171
172#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
173pub struct MergeFacesArgs {
174 pub bodies: Vec<ElementReference<NamedModel, Body>>,
176}
177
178part_operation!(
179 MergeFaces(MergeFacesArgs),
180 MoveFace(MoveFaceArgs),
181 OffsetFace(OffsetFaceArgs),
182 ReplaceFace(ReplaceFaceArgs),
183 DeleteFace(DeleteFaceArgs)
184);
185
186const fn info(label: &'static str, doc: &'static str) -> OperationInfo {
188 OperationInfo::new(label, doc, OperationGroup::Features, OperationTier::Menu)
189}
190
191impl PartOperation for MoveFace {
192 const KIND: &'static str = "move_face";
193 const INFO: OperationInfo = info(
194 "Move face",
195 "Shift faces of a solid, its neighbours trimmed or grown to meet them.",
196 )
197 .icon(crate::icons::MOVE_FACE);
198 type Args = MoveFaceArgs;
199 type Session = ();
200
201 fn new_args(_: &Context) -> MoveFaceArgs {
203 MoveFaceArgs {
204 faces: Vec::new(),
205 translation: [0.0; 3],
206 }
207 }
208
209 fn run(ctx: &mut StepContext, args: &MoveFaceArgs) -> GeopResult<()> {
210 let _step = ctx.step();
211 build_model::<Self>(ctx, args)
212 }
213
214 fn form<'a>(
217 context: &'a Context<'a>,
218 args: &MoveFaceArgs,
219 _: &FormState<()>,
220 ) -> EditForm<'a, MoveFaceArgs> {
221 let before = &context.before;
222 let mut f = EditForm::<MoveFaceArgs>::default();
223 let shift = Vector3::from_array(args.translation.map(Scalar::from_f64));
224 f.form.gizmo =
225 faces_middle(before, &args.faces).map(|c| Gizmo::new(c.add(&shift)).translate());
226 f.pick(
227 "faces",
228 "faces",
229 before,
230 &args.faces,
231 true,
232 |edit, faces| edit.args.faces = faces,
233 );
234 for (k, (key, label)) in [("x", "shift x"), ("y", "shift y"), ("z", "shift z")]
235 .into_iter()
236 .enumerate()
237 {
238 let number = Number::new(label, args.translation[k], Unit::Length);
239 f.number(key, number, move |edit, v| edit.args.translation[k] = v);
240 }
241 f
242 }
243
244 fn canvas(_: &Context, edit: Edit<'_, MoveFaceArgs, ()>, event: &CanvasEvent) {
246 if let CanvasEvent::Gizmo { drag, .. } = event
247 && let Change::Translate(by) = &drag.change
248 {
249 for (k, shift) in edit.args.translation.iter_mut().enumerate() {
250 *shift += by[k].to_f64();
251 }
252 }
253 }
254}
255
256impl ModelOperation for MoveFace {
257 fn build(
258 model: &mut NamedModel,
259 _: &Reader,
260 step: &str,
261 args: &MoveFaceArgs,
262 ) -> GeopResult<()> {
263 let namer = Namer::new("move_face", step)?;
264 for v in args.translation {
265 finite("shift", v)?;
266 }
267 let motion =
268 Motion::translation(Vector3::from_array(args.translation.map(Scalar::from_f64)));
269 let faces = face_ids(model, &args.faces)?;
270 replace_faces(model, &namer, &faces, Smooth::Follow, |_, surface| {
271 Ok(surface.transform(&motion))
272 })?;
273 Ok(())
274 }
275}
276
277impl PartOperation for OffsetFace {
278 const KIND: &'static str = "offset_face";
279 const INFO: OperationInfo = info(
280 "Offset face",
281 "Push or pull faces of a solid along their normals, its neighbours trimmed or grown to meet them.",
282 )
283 .icon(crate::icons::OFFSET_FACE);
284 type Args = OffsetFaceArgs;
285 type Session = ();
286
287 fn new_args(_: &Context) -> OffsetFaceArgs {
289 OffsetFaceArgs {
290 faces: Vec::new(),
291 distance: 1.0,
292 }
293 }
294
295 fn run(ctx: &mut StepContext, args: &OffsetFaceArgs) -> GeopResult<()> {
296 let _step = ctx.step();
297 build_model::<Self>(ctx, args)
298 }
299
300 fn form<'a>(
302 context: &'a Context<'a>,
303 args: &OffsetFaceArgs,
304 _: &FormState<()>,
305 ) -> EditForm<'a, OffsetFaceArgs> {
306 let mut f = EditForm::<OffsetFaceArgs>::default();
307 f.pick(
308 "faces",
309 "faces",
310 &context.before,
311 &args.faces,
312 true,
313 |edit, faces| edit.args.faces = faces,
314 );
315 let distance = Number::new("distance", args.distance, Unit::Length);
316 f.number("distance", distance, |edit, v| edit.args.distance = v);
317 f
318 }
319}
320
321impl ModelOperation for OffsetFace {
322 fn build(
323 model: &mut NamedModel,
324 _: &Reader,
325 step: &str,
326 args: &OffsetFaceArgs,
327 ) -> GeopResult<()> {
328 let namer = Namer::new("offset_face", step)?;
329 finite("distance", args.distance)?;
330 if args.distance == 0.0 {
331 return Err(GeopError::new(
332 "an offset of no distance leaves the faces where they are: give a distance",
333 ));
334 }
335 let distance = Scalar::from_f64(args.distance);
336 let faces = face_ids(model, &args.faces)?;
337 replace_faces(model, &namer, &faces, Smooth::Follow, |name, surface| {
338 surface
339 .offset(distance)
340 .map_err(|e| e.with_context(format!("offsetting face {name}")))?
341 .ok_or_else(|| {
342 GeopError::new(format!(
343 "face {name} collapses offset by {distance:?}: every point of it passes its centre of curvature"
344 ))
345 })
346 })?;
347 Ok(())
348 }
349}
350
351impl PartOperation for ReplaceFace {
352 const KIND: &'static str = "replace_face";
353 const INFO: OperationInfo = info(
354 "Replace face",
355 "Put a face of a solid on the surface of another face, its neighbours trimmed or grown to meet it.",
356 )
357 .icon(crate::icons::REPLACE_FACE);
358 type Args = ReplaceFaceArgs;
359 type Session = ();
360
361 fn new_args(_: &Context) -> ReplaceFaceArgs {
363 ReplaceFaceArgs {
364 face: ElementReference::new(model(), ""),
365 with: ElementReference::new(model(), ""),
366 }
367 }
368
369 fn run(ctx: &mut StepContext, args: &ReplaceFaceArgs) -> GeopResult<()> {
370 let _step = ctx.step();
371 build_model::<Self>(ctx, args)
372 }
373
374 fn form<'a>(
376 context: &'a Context<'a>,
377 args: &ReplaceFaceArgs,
378 _: &FormState<()>,
379 ) -> EditForm<'a, ReplaceFaceArgs> {
380 let before = &context.before;
381 let mut f = EditForm::<ReplaceFaceArgs>::default();
382 f.pick_one("face", "face", before, Some(&args.face), |edit, face| {
383 edit.args.face = face
384 });
385 f.pick_one("with", "onto", before, Some(&args.with), |edit, with| {
386 edit.args.with = with
387 });
388 f
389 }
390}
391
392impl ModelOperation for ReplaceFace {
393 fn build(
394 model: &mut NamedModel,
395 _: &Reader,
396 step: &str,
397 args: &ReplaceFaceArgs,
398 ) -> GeopResult<()> {
399 let namer = Namer::new("replace_face", step)?;
400 let face = model.face_id(args.face.name())?;
401 let with = model.face_id(args.with.name())?;
402 if face == with {
403 return Err(GeopError::new(
404 "a face replaced by its own surface stays where it is: pick another face",
405 ));
406 }
407 let topology = model.topology();
408 let corners: Vec<Vector3> = (topology.face_edges(face))
409 .map(|e| {
410 Ok(topology
411 .get_vertex(topology.get_edge(e)?.start_vertex)?
412 .point)
413 })
414 .collect::<GeopResult<_>>()?;
415 let target = topology.get_face(with)?.surface.covering(&corners)?;
416 let anchor = face_point(model, face)?;
417 replace_faces(model, &namer, &[face], Smooth::Refuse, |name, old| {
418 facing_as(&target, old, anchor)
419 .map_err(|e| e.with_context(format!("putting face {name} on another surface")))
420 })?;
421 Ok(())
422 }
423}
424
425impl PartOperation for MergeFaces {
426 const KIND: &'static str = "merge_faces";
427 const INFO: OperationInfo = info(
428 "Merge faces",
429 "Make faces that lie on one surface one face, and drop the edges and vertices between them.",
430 )
431 .icon(crate::icons::MERGE_FACES);
432 type Args = MergeFacesArgs;
433 type Session = ();
434
435 fn new_args(context: &Context) -> MergeFacesArgs {
437 MergeFacesArgs {
438 bodies: (read_model(&context.before).ok())
439 .and_then(|model| model.newest_solid())
440 .map(|name| ElementReference::new(model(), name))
441 .into_iter()
442 .collect(),
443 }
444 }
445
446 fn run(ctx: &mut StepContext, args: &MergeFacesArgs) -> GeopResult<()> {
447 let _step = ctx.step();
448 build_model::<Self>(ctx, args)
449 }
450
451 fn form<'a>(
453 context: &'a Context<'a>,
454 args: &MergeFacesArgs,
455 _: &FormState<()>,
456 ) -> EditForm<'a, MergeFacesArgs> {
457 let mut f = EditForm::<MergeFacesArgs>::default();
458 f.pick(
459 "bodies",
460 "bodies",
461 &context.before,
462 &args.bodies,
463 true,
464 |edit, picked| edit.args.bodies = picked,
465 );
466 f
467 }
468}
469
470impl ModelOperation for MergeFaces {
471 fn build(
472 model: &mut NamedModel,
473 reader: &Reader,
474 _: &str,
475 args: &MergeFacesArgs,
476 ) -> GeopResult<()> {
477 for body in resolve_bodies(reader, &args.bodies)? {
478 model.merge_faces_on_one_surface(body, |_| true, |_| true)?;
479 }
480 Ok(())
481 }
482}
483
484impl PartOperation for DeleteFace {
485 const KIND: &'static str = "delete_face";
486 const INFO: OperationInfo = info(
487 "Delete face",
488 "Take faces out of a solid: holes, bosses, chamfers and fillets healed over by the faces around them, or the solid left open.",
489 )
490 .icon(crate::icons::DELETE_FACE);
491 type Args = DeleteFaceArgs;
492 type Session = ();
493
494 fn new_args(_: &Context) -> DeleteFaceArgs {
496 DeleteFaceArgs {
497 faces: Vec::new(),
498 heal: HealGap::Heal,
499 whole_feature: false,
500 }
501 }
502
503 fn run(ctx: &mut StepContext, args: &DeleteFaceArgs) -> GeopResult<()> {
504 let _step = ctx.step();
505 build_model::<Self>(ctx, args)
506 }
507
508 fn form<'a>(
510 context: &'a Context<'a>,
511 args: &DeleteFaceArgs,
512 _: &FormState<()>,
513 ) -> EditForm<'a, DeleteFaceArgs> {
514 let mut f = EditForm::<DeleteFaceArgs>::default();
515 f.pick(
516 "faces",
517 "faces",
518 &context.before,
519 &args.faces,
520 true,
521 |edit, faces| edit.args.faces = faces,
522 );
523 let (heal, heals) = choices(&HealGap::ALL, &args.heal);
524 f.select("heal", "leave", heal, heals, false, |edit, value| {
525 if let Some(heal) = chosen(&HealGap::ALL, value) {
526 edit.args.heal = heal;
527 }
528 });
529 f.checkbox(
530 "whole_feature",
531 "whole feature",
532 args.whole_feature,
533 |edit, b| edit.args.whole_feature = b,
534 );
535 f
536 }
537}
538
539impl ModelOperation for DeleteFace {
540 fn build(
541 model: &mut NamedModel,
542 _: &Reader,
543 step: &str,
544 args: &DeleteFaceArgs,
545 ) -> GeopResult<()> {
546 let namer = Namer::new("delete_face", step)?;
547 let faces = face_ids(model, &args.faces)?;
548 let heal = match args.heal {
549 HealGap::Heal => Heal::Extend,
550 HealGap::Open => Heal::Open,
551 };
552 delete_faces(model, &namer, &faces, args.whole_feature, heal)?;
553 Ok(())
554 }
555}
556
557fn face_point(part: &NamedModel, face: FaceId) -> GeopResult<(Vector3, (Scalar, Scalar))> {
560 let model = part.topology();
561 let Some(coedge) = model.iterate_face_coedges(face).next() else {
562 return Err(GeopError::new(format!(
563 "face {} has no boundary",
564 part.name(face)?
565 )));
566 };
567 let uv = model.get_coedge(coedge)?.pcurve.ends()?.0;
568 let surface = &model.get_face(face)?.surface;
569 Ok((surface.evaluate(uv[0], uv[1])?, (uv[0], uv[1])))
570}
571
572fn facing_as(
577 surface: &NurbSurface3D,
578 old: &NurbSurface3D,
579 (point, (u, v)): (Vector3, (Scalar, Scalar)),
580) -> GeopResult<NurbSurface3D> {
581 let normal = old.normal(u, v)?;
582 let ((u0, u1), (v0, v1)) = (surface.domain_u(), surface.domain_v());
583 let half = Scalar::from_f64(0.5);
584 let (su, sv) = surface.project_in_domain(
585 point,
586 Scalar::interpolate(u0, u1, half).sharpen(),
587 Scalar::interpolate(v0, v1, half).sharpen(),
588 PROJECT_ITERATIONS,
589 )?;
590 let along = surface
591 .normal(su.sharpen(), sv.sharpen())?
592 .prod_dot(&normal);
593 if along.definitely_greater(Scalar::ZERO) {
594 Ok(surface.clone())
595 } else if along.definitely_less(Scalar::ZERO) {
596 Ok(surface.reverse_u())
597 } else {
598 Err(GeopError::new(format!(
599 "the new surface is square to the face at {point:?}: which way it faces cannot be told"
600 )))
601 }
602}