1use 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#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
38pub struct MoveBody;
39
40#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
41pub struct MoveBodyArgs {
42 pub bodies: Vec<BodyRef>,
44 #[serde(default)]
46 pub translation: [f64; 3],
47 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub axis: Option<TurnAxis>,
51 #[serde(default)]
53 pub angle: f64,
54 #[serde(default)]
56 pub copy: bool,
57 #[serde(default)]
59 pub combine: Combine,
60}
61
62impl MoveBodyArgs {
63 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
89const 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 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 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 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}