1use super::EngineState;
19use brep_kernel::{PmiAnnotation, PmiAnnotationReport, PmiGeometry, PmiPose};
20use serde_json::{json, Value};
21
22#[derive(Debug, Clone, Default, PartialEq)]
24pub struct ExplodePlayback {
25 pub progress: Option<f64>,
27 pub target: Option<(f64, f64)>,
29}
30
31#[derive(Debug, Clone, PartialEq)]
33pub struct ExplodeStep {
34 pub id: String,
35 pub targets: Vec<String>,
36 pub labels: Vec<String>,
39 pub translate: [f64; 3],
40 pub trace: bool,
41 pub enabled: bool,
42}
43
44#[derive(Debug, Clone, PartialEq)]
47pub struct HideStep {
48 pub id: String,
49 pub targets: Vec<String>,
50 pub labels: Vec<String>,
52 pub enabled: bool,
53}
54
55#[derive(Debug, Clone, PartialEq)]
57pub struct ExplodedView {
58 pub id: String,
59 pub name: String,
60 pub steps: Vec<ExplodeStep>,
61 pub hides: Vec<HideStep>,
63}
64
65pub(crate) fn explode_at(geometry: &PmiGeometry, fraction: f64) -> PmiGeometry {
69 match geometry {
70 PmiGeometry::Explode { solids, translate, rotate_deg, scale, center, trace } => PmiGeometry::Explode {
71 solids: solids.clone(),
72 translate: translate.map(|v| v * fraction),
73 rotate_deg: rotate_deg.map(|v| v * fraction),
74 scale: scale.map(|v| 1.0 + (v - 1.0) * fraction),
75 center: *center,
76 trace: *trace,
77 },
78 other => other.clone(),
79 }
80}
81
82pub(crate) struct PlaybackPoses<'a> {
88 progress: f64,
89 steps: Vec<&'a PmiAnnotationReport>,
91 index: std::collections::HashMap<&'a str, usize>,
93}
94
95fn pose_of(geometry: &PmiGeometry) -> Option<PmiPose> {
96 match geometry {
97 PmiGeometry::Explode { translate, rotate_deg, scale, center, .. } => {
98 Some(PmiPose { center: *center, translate: *translate, rotate_deg: *rotate_deg, scale: *scale })
99 }
100 _ => None,
101 }
102}
103
104impl PlaybackPoses<'_> {
105 fn stage(&self, step: usize) -> Option<(PmiPose, PmiPose)> {
107 let geometry = &self.steps[step].geometry;
108 let fraction = (self.progress - step as f64).clamp(0.0, 1.0);
109 Some((pose_of(geometry)?, pose_of(&explode_at(geometry, fraction))?))
110 }
111
112 fn stages_of(&self, solid: &str) -> Vec<(PmiPose, PmiPose)> {
114 (0..self.steps.len())
115 .filter(|&step| matches!(&self.steps[step].geometry, PmiGeometry::Explode { solids, .. } if solids.iter().any(|s| s == solid)))
116 .filter_map(|step| self.stage(step))
117 .collect()
118 }
119
120 fn rider(&self, row: &PmiAnnotationReport) -> Vec<(PmiPose, PmiPose)> {
125 match &row.geometry {
126 PmiGeometry::Explode { .. } => self.index.get(row.id.as_str()).and_then(|&step| self.stage(step)).into_iter().collect(),
127 PmiGeometry::Leader { balloon: true, anchor: Some(anchor), .. } => {
128 let solid = match &anchor.mode {
132 brep_kernel::BalloonAnchorMode::Edge { solid, .. } => Some(solid.as_str()),
133 _ => anchor.solids.iter().map(String::as_str).find(|solid| !self.stages_of(solid).is_empty()),
134 };
135 solid.map(|solid| self.stages_of(solid)).unwrap_or_default()
136 }
137 _ => Vec::new(),
138 }
139 }
140
141 pub(crate) fn shown(&self, row: &PmiAnnotationReport, point: [f64; 3]) -> [f64; 3] {
143 let stages = self.rider(row);
144 let model = stages.iter().rev().fold(point, |p, (full, _)| full.unapply(p));
145 stages.iter().fold(model, |p, (_, shown)| shown.apply(p))
146 }
147
148 pub(crate) fn full(&self, row: &PmiAnnotationReport, point: [f64; 3]) -> [f64; 3] {
151 let stages = self.rider(row);
152 let model = stages.iter().rev().fold(point, |p, (_, shown)| shown.unapply(p));
153 stages.iter().fold(model, |p, (full, _)| full.apply(p))
154 }
155
156 pub(crate) fn shown_geometry(&self, row: &PmiAnnotationReport) -> Option<PmiGeometry> {
159 match &row.geometry {
160 PmiGeometry::Leader { targets, dot, balloon: true, anchor } => Some(PmiGeometry::Leader {
161 targets: targets.iter().map(|target| self.shown(row, *target)).collect(),
162 dot: *dot,
163 balloon: true,
164 anchor: anchor.clone(),
165 }),
166 _ => None,
167 }
168 }
169}
170
171fn steps_of(annotations: &[PmiAnnotation], parts: &std::collections::HashMap<String, String>) -> Vec<ExplodeStep> {
172 annotations
173 .iter()
174 .filter(|a| a.kind == "explode")
175 .map(|a| {
176 let position = &a.params["transform"]["position"];
177 let translate = std::array::from_fn(|i| position[i].as_f64().unwrap_or(0.0));
178 let targets = a.references("targets");
179 let labels = targets
180 .iter()
181 .map(|t| parts.get(t).map_or_else(|| t.clone(), |part| format!("{part} ({t})")))
182 .collect();
183 ExplodeStep {
184 id: a.params["id"].as_str().unwrap_or_default().to_string(),
185 targets,
186 labels,
187 translate,
188 trace: a.params["showTraceLine"].as_bool().unwrap_or(true),
189 enabled: a.enabled,
190 }
191 })
192 .collect()
193}
194
195fn hides_of(annotations: &[PmiAnnotation], parts: &std::collections::HashMap<String, String>) -> Vec<HideStep> {
196 annotations
197 .iter()
198 .filter(|a| a.kind == "hide")
199 .map(|a| {
200 let targets = a.references("targets");
201 let labels = targets
202 .iter()
203 .map(|t| parts.get(t).map_or_else(|| t.clone(), |part| format!("{part} ({t})")))
204 .collect();
205 HideStep { id: a.params["id"].as_str().unwrap_or_default().to_string(), targets, labels, enabled: a.enabled }
206 })
207 .collect()
208}
209
210impl EngineState {
211 pub fn exploded_views(&self) -> Vec<ExplodedView> {
213 let state = self.pmi_state();
214 if !state.views.iter().any(|view| view.annotations.iter().any(|a| a.kind == "explode")) {
215 return Vec::new();
216 }
217 let mut parts: std::collections::HashMap<String, String> = (0..self.history.len())
220 .filter(|&i| self.history.feature_type(i).as_deref() == Some("ACOMP"))
221 .filter_map(|i| self.history.feature_params(i))
222 .filter_map(|p| Some((p["id"].as_str()?.to_string(), p["partName"].as_str()?.to_string())))
223 .collect();
224 for view in &state.views {
226 for target in view.annotations.iter().filter(|a| a.kind == "explode" || a.kind == "hide").flat_map(|a| a.references("targets")) {
227 if target.contains(':') && !parts.contains_key(&target) {
228 if let Some(part) = self.part_at_path(&target) {
229 parts.insert(target, part);
230 }
231 }
232 }
233 }
234 state
235 .views
236 .iter()
237 .map(|view| ExplodedView {
238 id: view.id.clone(),
239 name: view.name.clone(),
240 steps: steps_of(&view.annotations, &parts),
241 hides: hides_of(&view.annotations, &parts),
242 })
243 .filter(|view| !view.steps.is_empty())
244 .collect()
245 }
246
247 pub fn is_exploded_view(&self, id: &str) -> bool {
249 self.pmi_state().find_view(id).is_some_and(|view| view.annotations.iter().any(|a| a.kind == "explode"))
250 }
251
252 pub fn exploded_step_count(&self) -> usize {
255 let Some(active) = self.pmi_active_view.as_deref() else { return 0 };
256 self.pmi_report
257 .as_ref()
258 .and_then(|report| report.view(active))
259 .map_or(0, |view| {
260 view.annotations
261 .iter()
262 .filter(|row| row.enabled && row.status == brep_kernel::PmiStatus::Ok)
263 .filter(|row| matches!(row.geometry, PmiGeometry::Explode { .. }))
264 .count()
265 })
266 }
267
268 pub(crate) fn explode_fraction(&self, index: usize) -> f64 {
271 match self.explode_playback.progress {
272 None => 1.0,
273 Some(progress) => (progress - index as f64).clamp(0.0, 1.0),
274 }
275 }
276
277 pub(crate) fn explode_playback_poses(&self) -> Option<PlaybackPoses<'_>> {
280 let progress = self.explode_playback.progress?;
281 let active = self.pmi_active_view.as_deref()?;
282 let view = self.pmi_report.as_ref()?.view(active)?;
283 let steps: Vec<&PmiAnnotationReport> = view
284 .annotations
285 .iter()
286 .filter(|row| row.enabled && row.status == brep_kernel::PmiStatus::Ok)
287 .filter(|row| matches!(row.geometry, PmiGeometry::Explode { .. }))
288 .collect();
289 if progress >= steps.len() as f64 {
290 return None;
291 }
292 let index = steps.iter().enumerate().map(|(step, row)| (row.id.as_str(), step)).collect();
293 Some(PlaybackPoses { progress, steps, index })
294 }
295
296 pub(crate) fn explode_step_of(&self, id: &str) -> Option<usize> {
298 let active = self.pmi_active_view.as_deref()?;
299 self.pmi_report
300 .as_ref()?
301 .view(active)?
302 .annotations
303 .iter()
304 .filter(|row| row.enabled && row.status == brep_kernel::PmiStatus::Ok)
305 .filter(|row| matches!(row.geometry, PmiGeometry::Explode { .. }))
306 .position(|row| row.id == id)
307 }
308
309 pub fn exploded_progress(&self) -> f64 {
312 self.explode_playback.progress.unwrap_or(self.exploded_step_count() as f64)
313 }
314
315 pub fn exploded_state_json(&self) -> Value {
316 let steps = self.exploded_step_count();
317 json!({
318 "activeView": self.pmi_active_view,
319 "steps": steps,
320 "progress": self.explode_playback.progress.unwrap_or(steps as f64),
321 "animating": self.explode_playback.target.is_some(),
322 "views": self.exploded_views().iter().map(|view| json!({
323 "id": view.id,
324 "name": view.name,
325 "steps": view.steps.iter().map(|step| json!({
326 "id": step.id, "targets": step.targets, "labels": step.labels, "translate": step.translate,
327 "trace": step.trace, "enabled": step.enabled,
328 })).collect::<Vec<_>>(),
329 "hides": view.hides.iter().map(|step| json!({
330 "id": step.id, "targets": step.targets, "labels": step.labels, "enabled": step.enabled,
331 })).collect::<Vec<_>>(),
332 })).collect::<Vec<_>>(),
333 })
334 }
335
336 pub fn exploded_set_progress(&mut self, progress: f64) {
339 self.explode_playback.target = None;
340 self.set_explode_progress(progress);
341 }
342
343 fn set_explode_progress(&mut self, progress: f64) {
344 let steps = self.exploded_step_count() as f64;
345 self.explode_playback.progress = Some(progress.clamp(0.0, steps));
346 if self.pmi_active_view.is_some() {
347 self.pmi_apply_explode();
348 self.refresh_pmi_overlay();
349 self.sync_transform_gizmo();
351 }
352 }
353
354 pub fn exploded_animate(&mut self, explode: bool, steps_per_second: f64) -> Result<(), String> {
357 if self.pmi_active_view.is_none() {
358 return Err("activate an exploded view first".into());
359 }
360 let steps = self.exploded_step_count() as f64;
361 let to = if explode { steps } else { 0.0 };
362 if self.explode_playback.progress.is_none() {
363 self.set_explode_progress(if explode { 0.0 } else { steps });
365 }
366 self.explode_playback.target = Some((to, steps_per_second.max(1e-3)));
367 Ok(())
368 }
369
370 pub fn exploded_tick(&mut self, dt: f64) -> bool {
373 let Some((to, speed)) = self.explode_playback.target else { return false };
374 if self.pmi_active_view.is_none() {
375 self.explode_playback.target = None;
376 return false;
377 }
378 let from = self.explode_playback.progress.unwrap_or(to);
379 let step = speed * dt.max(0.0);
380 let next = if to > from { (from + step).min(to) } else { (from - step).max(to) };
381 self.set_explode_progress(next);
382 if (next - to).abs() < 1e-12 {
383 self.explode_playback.target = None;
384 return false;
385 }
386 true
387 }
388
389 pub fn selected_occurrences(&self) -> Vec<String> {
395 let selected: std::collections::HashSet<&str> = self.emphasis.selected_solids.iter().map(String::as_str).collect();
396 let mut out: Vec<String> = Vec::new();
397 for name in &self.emphasis.selected_solids {
398 let mut path = String::new();
399 for (at, _) in name.match_indices(':') {
400 path = name[..at].to_string();
401 let prefix = format!("{path}:");
402 if self.scene.solids().iter().filter(|s| s.name.starts_with(&prefix)).all(|s| selected.contains(s.name.as_str())) {
403 break;
404 }
405 }
406 if !path.is_empty() && !out.contains(&path) {
407 out.push(path);
408 }
409 }
410 out
411 }
412
413 pub fn exploded_add_step(&mut self, targets: &[String]) -> Result<String, String> {
418 let view = self
419 .pmi_active_view
420 .clone()
421 .filter(|id| self.is_exploded_view(id))
422 .ok_or("show an exploded view first")?;
423 let targets: Vec<String> = if targets.is_empty() { self.selected_occurrences() } else { targets.to_vec() };
424 if targets.is_empty() {
425 return Err("select the components to move".into());
426 }
427 let mut low = [f64::INFINITY; 3];
428 let mut high = [f64::NEG_INFINITY; 3];
429 for target in &targets {
430 let prefix = format!("{target}:");
431 for solid in self.scene.solids().iter().filter(|s| s.name.starts_with(&prefix)) {
432 for k in 0..3 {
433 low[k] = low[k].min(solid.bbox.min[k]);
434 high[k] = high[k].max(solid.bbox.max[k]);
435 }
436 }
437 }
438 if !low[0].is_finite() {
439 return Err(format!("'{}' has no solids", targets.join(", ")));
440 }
441 let size = (0..3).map(|k| (high[k] - low[k]).powi(2)).sum::<f64>().sqrt().max(1.0);
442 let mut state = self.pmi_state();
443 let step = state.next_id("EXP");
444 state.find_view_mut(&view).ok_or("the exploded view vanished")?.annotations.push(PmiAnnotation {
445 plugin_replay: None,
446 persistent_data: Value::Null,
447 kind: "explode".into(),
448 enabled: true,
449 params: json!({
450 "id": step,
451 "targets": targets,
452 "transform": { "position": [0.0, 0.0, size], "rotationEuler": [0, 0, 0], "scale": [1, 1, 1] },
453 "showTraceLine": true,
454 }),
455 label_world: None,
456 });
457 self.write_pmi_state(state);
458 self.explode_playback = ExplodePlayback::default();
459 self.pmi_set_annotation_open(Some(&step));
460 Ok(step)
461 }
462
463 pub fn assembly_auto_explode(&mut self, name: Option<&str>, spread: f64) -> Result<String, String> {
469 self.assembly_auto_explode_deep(name, spread, 1)
470 }
471
472 fn placements_under(&self, path: Option<&str>) -> Vec<(String, bool)> {
475 let of = |features: &[Value], prefix: &str| -> Vec<(String, bool)> {
476 features
477 .iter()
478 .filter(|f| f["type"] == "ACOMP")
479 .filter_map(|f| {
480 let id = f["inputParams"]["id"].as_str()?;
481 Some((format!("{prefix}{id}"), f["inputParams"]["isFixed"].as_bool().unwrap_or(false)))
482 })
483 .collect()
484 };
485 let Some(path) = path else {
486 return of(self.history.features(), "");
487 };
488 let mut segments = path.split(':');
489 let Some(top) = segments.next() else { return Vec::new() };
490 let Some(params) = self.history.index_of(top).and_then(|i| self.history.feature_params(i)) else { return Vec::new() };
491 let Some(mut part) = params["partName"].as_str().map(str::to_string) else { return Vec::new() };
492 let mut library = self.history.parts_library();
493 let mut document = &library[&part]["document"];
494 for id in segments {
495 let Some(next) = document["features"]
496 .as_array()
497 .and_then(|features| features.iter().find(|f| f["inputParams"]["id"].as_str() == Some(id)))
498 .and_then(|f| f["inputParams"]["partName"].as_str())
499 else {
500 return Vec::new();
501 };
502 part = next.to_string();
503 library = &document["partsLibrary"];
504 document = &library[&part]["document"];
505 }
506 of(document["features"].as_array().map(Vec::as_slice).unwrap_or(&[]), &format!("{path}:"))
507 }
508
509 fn explode_group(&self, placements: &[(String, bool)], spread: f64) -> Option<Vec<(String, f64, [f64; 3])>> {
514 let boxes: Vec<(&str, bool, [f64; 3], [f64; 3])> = placements
515 .iter()
516 .filter_map(|(path, fixed)| {
517 let prefix = format!("{path}:");
518 let mut low = [f64::INFINITY; 3];
519 let mut high = [f64::NEG_INFINITY; 3];
520 for solid in self.scene.solids().iter().filter(|s| s.name.starts_with(&prefix)) {
521 for k in 0..3 {
522 low[k] = low[k].min(solid.bbox.min[k]);
523 high[k] = high[k].max(solid.bbox.max[k]);
524 }
525 }
526 low[0].is_finite().then_some((path.as_str(), *fixed, low, high))
527 })
528 .collect();
529 if boxes.len() < 2 {
530 return None;
531 }
532 let volume = |b: &(&str, bool, [f64; 3], [f64; 3])| (0..3).map(|k| (b.3[k] - b.2[k]).max(0.0)).product::<f64>();
533 let base = boxes
534 .iter()
535 .position(|b| b.1)
536 .unwrap_or_else(|| (0..boxes.len()).max_by(|&a, &b| volume(&boxes[a]).total_cmp(&volume(&boxes[b]))).unwrap_or(0));
537 let centre = |b: &(&str, bool, [f64; 3], [f64; 3])| -> [f64; 3] { std::array::from_fn(|k| (b.2[k] + b.3[k]) / 2.0) };
538 let origin = centre(&boxes[base]);
539 let low: [f64; 3] = std::array::from_fn(|k| boxes.iter().map(|b| b.2[k]).fold(f64::INFINITY, f64::min));
540 let high: [f64; 3] = std::array::from_fn(|k| boxes.iter().map(|b| b.3[k]).fold(f64::NEG_INFINITY, f64::max));
541 let diagonal = (0..3).map(|k| (high[k] - low[k]).powi(2)).sum::<f64>().sqrt().max(1e-9);
542 let distance = spread * diagonal / 2.0;
543 let mut moves: Vec<(String, f64, [f64; 3])> = boxes
544 .iter()
545 .enumerate()
546 .filter(|(i, _)| *i != base)
547 .map(|(_, b)| {
548 let c = centre(b);
549 let d: [f64; 3] = std::array::from_fn(|k| c[k] - origin[k]);
550 let length = d.iter().map(|v| v * v).sum::<f64>().sqrt();
551 let direction = if length > 1e-6 * diagonal { d.map(|v| v / length) } else { [0.0, 0.0, 1.0] };
552 (b.0.to_string(), length, direction.map(|v| v * distance))
553 })
554 .collect();
555 moves.sort_by(|a, b| b.1.total_cmp(&a.1));
557 Some(moves)
558 }
559
560 pub fn assembly_auto_explode_deep(&mut self, name: Option<&str>, spread: f64, depth: usize) -> Result<String, String> {
567 if !(spread.is_finite() && spread > 0.0) {
568 return Err("the spread must be a positive number".into());
569 }
570 self.pmi_deactivate_view();
572 let mut moves: Vec<(String, f64, [f64; 3])> = Vec::new();
573 let mut level: Vec<Option<String>> = vec![None];
574 for _ in 0..depth.max(1) {
575 let mut next = Vec::new();
576 for parent in &level {
577 let placements = self.placements_under(parent.as_deref());
578 if let Some(group) = self.explode_group(&placements, spread) {
579 moves.extend(group);
580 }
581 next.extend(placements.into_iter().map(|(path, _)| Some(path)));
582 }
583 level = next;
584 }
585 if moves.is_empty() {
586 return Err("an exploded view needs at least two placed components".into());
587 }
588 let name = name
589 .map(str::trim)
590 .filter(|n| !n.is_empty())
591 .map(String::from)
592 .unwrap_or_else(|| format!("Exploded view {}", self.exploded_views().len() + 1));
593 let mut exploded = crate::camera::Aabb::empty();
600 for solid in self.scene.solids().iter().filter(|s| s.visible) {
601 let mut shift = [0.0; 3];
602 for (path, _, translate) in &moves {
603 if solid.name.starts_with(path.as_str()) && solid.name[path.len()..].starts_with(':') {
604 for k in 0..3 {
605 shift[k] += translate[k];
606 }
607 }
608 }
609 exploded.expand(std::array::from_fn(|k| solid.bbox.min[k] + shift[k]));
610 exploded.expand(std::array::from_fn(|k| solid.bbox.max[k] + shift[k]));
611 }
612 let camera = self.camera.clone();
613 self.camera.zoom_to_fit(&exploded, 1.15);
614 let id = self.pmi_capture_view(Some(&name));
615 self.camera = camera;
616 let mut state = self.pmi_state();
617 let mut rows = Vec::new();
618 for (component, _, translate) in moves {
619 let step = state.next_id("EXP");
620 rows.push(PmiAnnotation {
621 plugin_replay: None,
622 persistent_data: Value::Null,
623 kind: "explode".into(),
624 enabled: true,
625 params: json!({
626 "id": step,
627 "targets": [component],
628 "transform": { "position": translate, "rotationEuler": [0, 0, 0], "scale": [1, 1, 1] },
629 "showTraceLine": true,
630 }),
631 label_world: None,
632 });
633 }
634 state.find_view_mut(&id).ok_or("the new view vanished")?.annotations.extend(rows);
635 self.write_pmi_state(state);
636 self.explode_playback = ExplodePlayback::default();
637 let _ = self.pmi_activate_view(&id);
638 Ok(id)
639 }
640}
641