Skip to main content

geop_ops_pattern/
move_body.rs

1//! [`MoveBody`]: move bodies, or a copy of them.
2
3use geop_core_editor::{
4    edit_form::{Edit, EditForm, FormState},
5    editable::{Context, OperationGroup, OperationInfo, OperationTier},
6    editing::{CanvasEvent, Change, Gizmo, Number, Unit},
7    part_operation,
8    part_operation::PartOperation,
9};
10use geop_core_math::{
11    geop_error::{GeopError, GeopResult},
12    primitives::{Motion, Pose},
13    scalars::Scalar,
14    vector::Vector3,
15};
16use geop_core_program::{part::StepContext, reader::Reader};
17use geop_ops_model::Combine;
18use geop_ops_model::{ModelOperation, NamedModel, Namer, build_model, read_model, resolve_bodies};
19use serde::{Deserialize, Serialize};
20
21use crate::{
22    common::{BodyRef, bodies_field, body, center, combine_instances, copy_seeds, seed_instances},
23    references::TurnAxis,
24};
25
26/// Moves the bodies `bodies` — solids, and sheets by one of their faces —
27/// turned by `angle` degrees about `axis`, if any (counter-clockwise
28/// looking against it), then shifted by `translation`. Moved, every entity
29/// keeps its name; with `copy`, the bodies stay and a moved copy of them is
30/// made, the copy of each entity named `X` named `move(M,X)` for the
31/// operation `M`.
32///
33/// What is moved — the copy, or the bodies themselves — is kept as it is,
34/// or combined as [`Combine`] says, the result then named `move(M)`: a copy
35/// joined to the bodies, or the bodies moved into place and cut from
36/// another solid.
37#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
38pub struct MoveBody;
39
40#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
41pub struct MoveBodyArgs {
42    /// The bodies to move: solids, or a face of each sheet.
43    pub bodies: Vec<BodyRef>,
44    /// The shift, along the world's `x`, `y` and `z`.
45    #[serde(default)]
46    pub translation: [f64; 3],
47    /// What to turn about, before the shift: a line, or a round entity's
48    /// axis; none, to shift only.
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub axis: Option<TurnAxis>,
51    /// How far to turn about `axis`, in degrees.
52    #[serde(default)]
53    pub angle: f64,
54    /// Move a copy, keeping the bodies where they are.
55    #[serde(default)]
56    pub copy: bool,
57    /// Keep what is moved as it is, or combine it with a solid.
58    #[serde(default)]
59    pub combine: Combine,
60}
61
62impl MoveBodyArgs {
63    /// The motion it moves by, as `reader` reads the part: none, if it
64    /// moves nothing.
65    fn motion(&self, reader: &Reader) -> GeopResult<Option<Motion>> {
66        let values = self.translation.iter().chain([&self.angle]);
67        if let Some(bad) = values.clone().find(|v| !v.is_finite()) {
68            return Err(GeopError::new(format!("{bad} is not a number")));
69        }
70        let shift = Vector3::from_array(self.translation.map(Scalar::from_f64));
71        let shifts = self.translation.iter().any(|&t| t != 0.0);
72        let turn = match &self.axis {
73            Some(axis) if self.angle != 0.0 => {
74                let axis = axis.resolve(reader)?;
75                let radians = Scalar::from_f64(self.angle).degrees_to_radians()?;
76                Some(Pose::rotation_about(&axis.point, &axis.direction, radians)?)
77            }
78            _ => None,
79        };
80        Ok(match (turn, shifts) {
81            (None, false) => None,
82            (None, true) => Some(Motion::translation(shift)),
83            (Some(turn), false) => Some(turn.motion()),
84            (Some(turn), true) => Some(turn.with_position(turn.position().add(&shift)).motion()),
85        })
86    }
87}
88
89/// The keys and labels of the shift's three fields.
90const AXES: [(&str, &str); 3] = [("x", "shift x"), ("y", "shift y"), ("z", "shift z")];
91
92part_operation!(MoveBody(MoveBodyArgs));
93
94impl PartOperation for MoveBody {
95    const KIND: &'static str = "move_body";
96    const INFO: OperationInfo = OperationInfo::new(
97        "Move body",
98        "Move bodies, or a copy of them, turned and shifted.",
99        OperationGroup::Bodies,
100        OperationTier::Menu,
101    )
102    .shortcut("Shift+M")
103    .icon(crate::icons::MOVE_BODY);
104    type Args = MoveBodyArgs;
105    type Session = ();
106
107    /// The newest solid, not moved yet.
108    fn new_args(context: &Context) -> MoveBodyArgs {
109        MoveBodyArgs {
110            bodies: (read_model(&context.before).ok())
111                .and_then(|model| model.newest_solid())
112                .map(body)
113                .into_iter()
114                .collect(),
115            translation: [0.0; 3],
116            axis: None,
117            angle: 0.0,
118            copy: false,
119            combine: Combine::NewBody,
120        }
121    }
122
123    fn run(ctx: &mut StepContext, args: &MoveBodyArgs) -> GeopResult<()> {
124        let _step = ctx.step();
125        build_model::<Self>(ctx, args)
126    }
127
128    /// The bodies, picked; the shift along each axis, dragged by a gizmo
129    /// where the bodies' middle is moved to; the axis to turn about and the
130    /// angle; whether to copy; and how to combine.
131    fn form<'a>(
132        context: &'a Context<'a>,
133        args: &MoveBodyArgs,
134        _: &FormState<()>,
135    ) -> EditForm<'a, MoveBodyArgs> {
136        let before = &context.before;
137        let mut f = EditForm::<MoveBodyArgs>::default();
138        let shift = Vector3::from_array(args.translation.map(Scalar::from_f64));
139        f.form.gizmo = center(before, &args.bodies).map(|c| Gizmo::new(c.add(&shift)).translate());
140        bodies_field(&mut f, before, &args.bodies, |edit, picked| {
141            edit.args.bodies = picked
142        });
143        for (k, (key, label)) in AXES.into_iter().enumerate() {
144            let number = Number::new(label, args.translation[k], Unit::Length);
145            f.number(key, number, move |edit, v| edit.args.translation[k] = v);
146        }
147        f.pick_one(
148            "axis",
149            "turn about",
150            before,
151            args.axis.as_ref(),
152            |edit, picked| edit.args.axis = Some(picked),
153        );
154        f.form.dialog.optional("axis");
155        if args.axis.is_some() {
156            let angle = Number::new("angle", args.angle, Unit::Angle).range(-360.0, 360.0);
157            f.number("angle", angle, |edit, v| edit.args.angle = v);
158        }
159        f.checkbox("copy", "copy", args.copy, |edit, b| edit.args.copy = b);
160        let set_combine = |args: &mut MoveBodyArgs, combine| args.combine = combine;
161        args.combine.fields(&mut f, before, set_combine);
162        f
163    }
164
165    /// The gizmo dragged: the shift, as it was when the drag started, moved
166    /// by as much.
167    fn canvas(_: &Context, edit: Edit<'_, MoveBodyArgs, ()>, event: &CanvasEvent) {
168        if let CanvasEvent::Gizmo { drag, .. } = event
169            && let Change::Translate(by) = &drag.change
170        {
171            for (k, shift) in edit.args.translation.iter_mut().enumerate() {
172                *shift += by[k].to_f64();
173            }
174        }
175    }
176}
177
178impl ModelOperation for MoveBody {
179    fn build(
180        model: &mut NamedModel,
181        reader: &Reader,
182        step: &str,
183        args: &MoveBodyArgs,
184    ) -> GeopResult<()> {
185        let namer = Namer::new("move", step)?;
186        let seeds = resolve_bodies(reader, &args.bodies)?;
187        let motion = args.motion(reader)?;
188        let instances = match (args.copy, motion) {
189            (true, None) => {
190                return Err(GeopError::new(
191                    "a copy that is not moved lies on top of the bodies: shift or turn it",
192                ));
193            }
194            (true, Some(motion)) => {
195                copy_seeds(model, &seeds, &motion, "copy", |name| namer.name(&[name]))?
196            }
197            (false, motion) => {
198                if let Some(motion) = motion {
199                    for &seed in &seeds {
200                        model.transform_body(seed, &motion)?;
201                    }
202                }
203                seed_instances(&seeds, "moved")
204            }
205        };
206        combine_instances(model, &namer, step, &args.combine, &instances)
207    }
208}