1use geop_core_editor::{
15 edit_form::{Edit, EditForm, FormState},
16 editable::{Context, OperationGroup, OperationInfo, OperationTier},
17 editing::{CanvasEvent, Choice, ListItem, Number, Shape, Style, Tone, Track, Unit, Visual},
18 part_operation,
19 part_operation::PartOperation,
20 view::{Annotations, ViewAnnotation, annotations},
21};
22use geop_core_math::{
23 geop_error::{GeopError, GeopResult, WithContext},
24 primitives::Pose,
25 scalars::Scalar,
26 vector::Vector3,
27 with_context,
28};
29use geop_core_program::{element::ElementReference, part::StepContext, reader::Reader};
30use geop_ops_model::{
31 mate_entity::MateEntity,
32 placed::{
33 drawn,
34 explode::{Explosion, exploded, explosion, explosions},
35 locate, placed_parts,
36 },
37};
38use serde::{Deserialize, Serialize};
39
40#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
43pub struct Explode;
44
45#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
50#[serde(rename_all = "snake_case")]
51pub enum Direction {
52 X,
53 Y,
54 #[default]
55 Z,
56 Along(MateEntity),
57}
58
59#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
60pub struct ExplodeArgs {
61 #[serde(default)]
64 pub parts: Vec<String>,
65 #[serde(default)]
66 pub direction: Direction,
67 #[serde(default)]
69 pub reversed: bool,
70 #[serde(default)]
72 pub distance: f64,
73 #[serde(default)]
76 pub angle: f64,
77 #[serde(default = "trailed")]
79 pub trails: bool,
80}
81
82fn trailed() -> bool {
83 true
84}
85
86impl Default for ExplodeArgs {
87 fn default() -> Self {
88 ExplodeArgs {
89 parts: Vec::new(),
90 direction: Direction::Z,
91 reversed: false,
92 distance: 0.0,
93 angle: 0.0,
94 trails: true,
95 }
96 }
97}
98
99fn direction(reader: &Reader, args: &ExplodeArgs) -> GeopResult<Vector3> {
101 let axis = |k: usize| Vector3::axis(k);
102 let along = match &args.direction {
103 Direction::X => axis(0),
104 Direction::Y => axis(1),
105 Direction::Z => axis(2),
106 Direction::Along(entity) => *entity.frame_in_world(reader)?.w(),
107 };
108 Ok(if args.reversed { along.neg() } else { along })
109}
110
111fn middle(reader: &Reader, parts: &[String], before: &[&Explosion]) -> GeopResult<Vector3> {
115 let mut bounds: Option<[Scalar; 3]> = None;
116 for part in parts {
117 let at = centre_of(reader, part, before)?;
118 let at = at.to_array();
119 bounds = Some(match bounds {
120 Some(b) => std::array::from_fn(|k| b[k].union(at[k])),
121 None => at,
122 });
123 }
124 let bounds = bounds.ok_or_else(|| GeopError::new("no part to move"))?;
125 Ok(Vector3::from_array(bounds.map(|b| b.midpoint())))
126}
127
128fn centre_of(reader: &Reader, path: &str, before: &[&Explosion]) -> GeopResult<Vector3> {
131 let located = locate(reader, path)?;
132 let centre = drawn(reader, path)?.view.extent.center;
133 Ok(exploded(before, path, &located.world).apply(¢re))
134}
135
136fn check(reader: &Reader, args: &ExplodeArgs) -> GeopResult<()> {
139 if args.parts.is_empty() {
140 return Err(GeopError::new(
141 "click the placed parts to move: none is chosen yet",
142 ));
143 }
144 for part in &args.parts {
145 if locate(reader, part).is_err() {
146 return Err(GeopError::new(format!(
147 "{part:?} is no part placed so far: move one an earlier step placed"
148 )));
149 }
150 }
151 Ok(())
152}
153
154fn plan(reader: &Reader, args: &ExplodeArgs) -> GeopResult<Explosion> {
157 check(reader, args)?;
158 let along = direction(reader, args)?;
159 let offset = along.prod_scalar(Scalar::from_f64(args.distance));
160 let slide = Pose::identity().with_position(offset);
161 let motion = match args.angle == 0.0 {
164 true => slide,
165 false => {
166 let before = explosions(reader);
167 let middle = middle(reader, &args.parts, &before)?;
168 let radians = Scalar::from_f64(args.angle).degrees_to_radians()?;
169 slide.compose(&Pose::rotation_about(&middle, &along, radians)?)
170 }
171 };
172 Ok(Explosion {
173 parts: args.parts.clone(),
174 motion,
175 })
176}
177
178fn trails(reader: &Reader, (step, args): &(String, ExplodeArgs)) -> GeopResult<Annotations> {
181 if !args.trails {
182 return Ok(Annotations::default());
183 }
184 let moved = reader.retrieve_element(&ElementReference::whole(explosion(step)))?;
185 let before: Vec<&Explosion> = (explosions(reader).into_iter())
186 .filter(|e| !std::ptr::eq(*e, moved))
187 .collect();
188 let lines = (moved.parts.iter())
189 .map(|part| {
190 let from = centre_of(reader, part, &before)?;
191 Ok(ViewAnnotation {
192 name: format!("explode({step},{part})"),
193 label: format!("trail of {part}"),
194 polyline: vec![from, moved.motion.apply(&from)],
195 })
196 })
197 .collect::<GeopResult<Vec<_>>>()?;
198 Ok(Annotations(lines))
199}
200
201#[derive(Clone, Debug, Default)]
204pub struct ExplodeSession {
205 hover: Option<String>,
206 grabbed: Option<f64>,
207 along: bool,
209}
210
211fn moved_key(part: &str) -> String {
213 format!("move:{part}")
214}
215
216fn part_at(context: &Context, pointer: &geop_core_editor::editing::Pointer) -> Option<String> {
219 let scene = context.scene?;
220 let reader = context.built.as_ref().unwrap_or(&context.before);
221 (placed_parts(reader).ok()?.into_iter())
222 .filter_map(|placed| Some((scene.hit_instance(&placed.name, pointer)?, placed.name)))
223 .min_by(|a, b| a.0.to_f64().total_cmp(&b.0.to_f64()))
224 .map(|(_, name)| name)
225}
226
227part_operation!(Explode(ExplodeArgs));
228
229impl PartOperation for Explode {
230 const KIND: &'static str = "explode";
231 const INFO: OperationInfo = OperationInfo::new(
232 "Explode",
233 "Move placed parts off where they are assembled, along a direction and turned about it — one move of an exploded view, a line trailing each part.",
234 OperationGroup::Assembly,
235 OperationTier::Small,
236 )
237 .icon(crate::icons::EXPLODE);
238 type Args = ExplodeArgs;
239 type Session = ExplodeSession;
240
241 fn new_args(_: &Context) -> ExplodeArgs {
243 ExplodeArgs::default()
244 }
245
246 fn run(ctx: &mut StepContext, args: &ExplodeArgs) -> GeopResult<()> {
248 let step = ctx.step().to_string();
249 let context = with_context!("explode({step}, parts={:?})", args.parts);
250 ctx.produce(&explosion(&step), args, plan)
251 .with_context(context)?;
252 let trailed = (step.clone(), args.clone());
253 ctx.produce(&annotations(&step), &trailed, trails)
254 .with_context(context)
255 }
256
257 fn form<'a>(
260 context: &'a Context<'a>,
261 args: &ExplodeArgs,
262 editing: &FormState<ExplodeSession>,
263 ) -> EditForm<'a, ExplodeArgs, ExplodeSession> {
264 let mut f = EditForm::<ExplodeArgs, ExplodeSession>::default();
265 let before = &context.before;
266 f.form.dialog.text(
267 "hint",
268 "Click the placed parts to move, then drag one of them, or the arrow, along the direction.",
269 Tone::Hint,
270 );
271 let items = (args.parts.iter())
272 .map(|part| {
273 let key = format!("part:{part}");
274 let moved = part.clone();
275 f.on(key.clone(), move |Edit { args, .. }, value| {
276 if matches!(value, geop_core_editor::editing::Value::Remove) {
277 args.parts.retain(|p| *p != moved);
278 }
279 });
280 let mut item = ListItem::new(key, part.clone());
281 item.removable = true;
282 item
283 })
284 .collect();
285 f.form
286 .dialog
287 .list("parts", items, "None yet: click a placed part.");
288
289 let way = match &args.direction {
290 _ if editing.session.along => "along",
291 Direction::X => "x",
292 Direction::Y => "y",
293 Direction::Z => "z",
294 Direction::Along(_) => "along",
295 };
296 let choices = vec![
297 Choice::new("z", "up (z)"),
298 Choice::new("x", "along x"),
299 Choice::new("y", "along y"),
300 Choice::new("along", "along what is picked"),
301 ];
302 f.select(
303 "direction",
304 "direction",
305 way,
306 choices,
307 false,
308 |Edit { args, session, .. }, way| {
309 session.along = way == "along";
310 args.direction = match way {
311 "x" => Direction::X,
312 "y" => Direction::Y,
313 "z" => Direction::Z,
314 _ => args.direction.clone(),
316 };
317 },
318 );
319 if way == "along" {
320 let along = match &args.direction {
321 Direction::Along(entity) => Some(entity),
322 _ => None,
323 };
324 f.pick_one("along", "along", before, along, |e, entity| {
325 e.args.direction = Direction::Along(entity);
326 e.session.along = false;
327 });
328 }
329 f.checkbox(
330 "reversed",
331 "the other way",
332 args.reversed,
333 |Edit { args, .. }, reversed| args.reversed = reversed,
334 );
335 let shown = context.built.as_ref().unwrap_or(before);
338 let handle = (|| -> GeopResult<Track> {
339 let along = direction(shown, args)?;
340 let all = explosions(shown);
341 let at = middle(shown, &args.parts, &all)?;
342 Ok(Track {
343 at,
344 direction: along,
345 })
346 })()
347 .ok();
348 f.number(
349 "distance",
350 Number::new("distance", args.distance, Unit::Length).handle(handle),
351 |Edit { args, .. }, distance| args.distance = distance,
352 );
353 f.number(
354 "angle",
355 Number::new("turned", args.angle, Unit::Angle),
356 |Edit { args, .. }, angle| args.angle = angle,
357 );
358 f.checkbox(
359 "trails",
360 "trail lines",
361 args.trails,
362 |Edit { args, .. }, trails| args.trails = trails,
363 );
364 for part in &args.parts {
365 let shape = Shape::Instance { name: part.clone() };
366 f.form
367 .visuals
368 .push(Visual::new(moved_key(part), shape, Style::Selected).draggable());
369 }
370 if let Some(hover) = &editing.session.hover
371 && !args.parts.contains(hover)
372 {
373 let shape = Shape::Instance {
374 name: hover.clone(),
375 };
376 f.form
377 .visuals
378 .push(Visual::new("hover", shape, Style::Hover));
379 }
380 f
381 }
382
383 fn canvas(context: &Context, edit: Edit<'_, ExplodeArgs, ExplodeSession>, event: &CanvasEvent) {
387 let Edit { args, session, .. } = edit;
388 match event {
389 CanvasEvent::Hover { pointer, .. } => session.hover = part_at(context, pointer),
390 CanvasEvent::Leave | CanvasEvent::Cancel => session.hover = None,
391 CanvasEvent::Click { pointer, .. } => {
392 if let Some(part) = part_at(context, pointer) {
393 match args.parts.iter().position(|p| *p == part) {
394 Some(k) => {
395 args.parts.remove(k);
396 }
397 None => args.parts.push(part),
398 }
399 }
400 }
401 CanvasEvent::Move {
402 key,
403 from,
404 to,
405 done,
406 ..
407 } if key.starts_with("move:") => {
408 let shown = context.built.as_ref().unwrap_or(&context.before);
409 let Ok(along) = direction(shown, args) else {
410 return;
411 };
412 let start = *session.grabbed.get_or_insert(args.distance);
413 let slid = to.sub(from).prod_dot(&along).to_f64();
416 args.distance = start + slid;
417 if *done {
418 session.grabbed = None;
419 }
420 }
421 _ => {}
422 }
423 }
424}