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)]
46pub struct ExplodedView {
47 pub id: String,
48 pub name: String,
49 pub steps: Vec<ExplodeStep>,
50}
51
52pub(crate) fn explode_at(geometry: &PmiGeometry, fraction: f64) -> PmiGeometry {
56 match geometry {
57 PmiGeometry::Explode { solids, translate, rotate_deg, scale, center, trace } => PmiGeometry::Explode {
58 solids: solids.clone(),
59 translate: translate.map(|v| v * fraction),
60 rotate_deg: rotate_deg.map(|v| v * fraction),
61 scale: scale.map(|v| 1.0 + (v - 1.0) * fraction),
62 center: *center,
63 trace: *trace,
64 },
65 other => other.clone(),
66 }
67}
68
69pub(crate) struct PlaybackPoses<'a> {
75 progress: f64,
76 steps: Vec<&'a PmiAnnotationReport>,
78 index: std::collections::HashMap<&'a str, usize>,
80}
81
82fn pose_of(geometry: &PmiGeometry) -> Option<PmiPose> {
83 match geometry {
84 PmiGeometry::Explode { translate, rotate_deg, scale, center, .. } => {
85 Some(PmiPose { center: *center, translate: *translate, rotate_deg: *rotate_deg, scale: *scale })
86 }
87 _ => None,
88 }
89}
90
91impl PlaybackPoses<'_> {
92 fn stage(&self, step: usize) -> Option<(PmiPose, PmiPose)> {
94 let geometry = &self.steps[step].geometry;
95 let fraction = (self.progress - step as f64).clamp(0.0, 1.0);
96 Some((pose_of(geometry)?, pose_of(&explode_at(geometry, fraction))?))
97 }
98
99 fn stages_of(&self, solid: &str) -> Vec<(PmiPose, PmiPose)> {
101 (0..self.steps.len())
102 .filter(|&step| matches!(&self.steps[step].geometry, PmiGeometry::Explode { solids, .. } if solids.iter().any(|s| s == solid)))
103 .filter_map(|step| self.stage(step))
104 .collect()
105 }
106
107 fn rider(&self, row: &PmiAnnotationReport) -> Vec<(PmiPose, PmiPose)> {
112 match &row.geometry {
113 PmiGeometry::Explode { .. } => self.index.get(row.id.as_str()).and_then(|&step| self.stage(step)).into_iter().collect(),
114 PmiGeometry::Leader { balloon: true, anchor: Some(anchor), .. } => {
115 let solid = match &anchor.mode {
119 brep_kernel::BalloonAnchorMode::Edge { solid, .. } => Some(solid.as_str()),
120 _ => anchor.solids.iter().map(String::as_str).find(|solid| !self.stages_of(solid).is_empty()),
121 };
122 solid.map(|solid| self.stages_of(solid)).unwrap_or_default()
123 }
124 _ => Vec::new(),
125 }
126 }
127
128 pub(crate) fn shown(&self, row: &PmiAnnotationReport, point: [f64; 3]) -> [f64; 3] {
130 let stages = self.rider(row);
131 let model = stages.iter().rev().fold(point, |p, (full, _)| full.unapply(p));
132 stages.iter().fold(model, |p, (_, shown)| shown.apply(p))
133 }
134
135 pub(crate) fn full(&self, row: &PmiAnnotationReport, point: [f64; 3]) -> [f64; 3] {
138 let stages = self.rider(row);
139 let model = stages.iter().rev().fold(point, |p, (_, shown)| shown.unapply(p));
140 stages.iter().fold(model, |p, (full, _)| full.apply(p))
141 }
142
143 pub(crate) fn shown_geometry(&self, row: &PmiAnnotationReport) -> Option<PmiGeometry> {
146 match &row.geometry {
147 PmiGeometry::Leader { targets, dot, balloon: true, anchor } => Some(PmiGeometry::Leader {
148 targets: targets.iter().map(|target| self.shown(row, *target)).collect(),
149 dot: *dot,
150 balloon: true,
151 anchor: anchor.clone(),
152 }),
153 _ => None,
154 }
155 }
156}
157
158fn steps_of(annotations: &[PmiAnnotation], parts: &std::collections::HashMap<String, String>) -> Vec<ExplodeStep> {
159 annotations
160 .iter()
161 .filter(|a| a.kind == "explode")
162 .map(|a| {
163 let position = &a.params["transform"]["position"];
164 let translate = std::array::from_fn(|i| position[i].as_f64().unwrap_or(0.0));
165 let targets = a.references("targets");
166 let labels = targets
167 .iter()
168 .map(|t| parts.get(t).map_or_else(|| t.clone(), |part| format!("{part} ({t})")))
169 .collect();
170 ExplodeStep {
171 id: a.params["id"].as_str().unwrap_or_default().to_string(),
172 targets,
173 labels,
174 translate,
175 trace: a.params["showTraceLine"].as_bool().unwrap_or(true),
176 enabled: a.enabled,
177 }
178 })
179 .collect()
180}
181
182impl EngineState {
183 pub fn exploded_views(&self) -> Vec<ExplodedView> {
185 let state = self.pmi_state();
186 if !state.views.iter().any(|view| view.annotations.iter().any(|a| a.kind == "explode")) {
187 return Vec::new();
188 }
189 let mut parts: std::collections::HashMap<String, String> = (0..self.history.len())
192 .filter(|&i| self.history.feature_type(i).as_deref() == Some("ACOMP"))
193 .filter_map(|i| self.history.feature_params(i))
194 .filter_map(|p| Some((p["id"].as_str()?.to_string(), p["partName"].as_str()?.to_string())))
195 .collect();
196 for view in &state.views {
198 for target in view.annotations.iter().filter(|a| a.kind == "explode").flat_map(|a| a.references("targets")) {
199 if target.contains(':') && !parts.contains_key(&target) {
200 if let Some(part) = self.part_at_path(&target) {
201 parts.insert(target, part);
202 }
203 }
204 }
205 }
206 state
207 .views
208 .iter()
209 .map(|view| ExplodedView { id: view.id.clone(), name: view.name.clone(), steps: steps_of(&view.annotations, &parts) })
210 .filter(|view| !view.steps.is_empty())
211 .collect()
212 }
213
214 pub fn is_exploded_view(&self, id: &str) -> bool {
216 self.pmi_state().find_view(id).is_some_and(|view| view.annotations.iter().any(|a| a.kind == "explode"))
217 }
218
219 pub fn exploded_step_count(&self) -> usize {
222 let Some(active) = self.pmi_active_view.as_deref() else { return 0 };
223 self.pmi_report
224 .as_ref()
225 .and_then(|report| report.view(active))
226 .map_or(0, |view| {
227 view.annotations
228 .iter()
229 .filter(|row| row.enabled && row.status == brep_kernel::PmiStatus::Ok)
230 .filter(|row| matches!(row.geometry, PmiGeometry::Explode { .. }))
231 .count()
232 })
233 }
234
235 pub(crate) fn explode_fraction(&self, index: usize) -> f64 {
238 match self.explode_playback.progress {
239 None => 1.0,
240 Some(progress) => (progress - index as f64).clamp(0.0, 1.0),
241 }
242 }
243
244 pub(crate) fn explode_playback_poses(&self) -> Option<PlaybackPoses<'_>> {
247 let progress = self.explode_playback.progress?;
248 let active = self.pmi_active_view.as_deref()?;
249 let view = self.pmi_report.as_ref()?.view(active)?;
250 let steps: Vec<&PmiAnnotationReport> = view
251 .annotations
252 .iter()
253 .filter(|row| row.enabled && row.status == brep_kernel::PmiStatus::Ok)
254 .filter(|row| matches!(row.geometry, PmiGeometry::Explode { .. }))
255 .collect();
256 if progress >= steps.len() as f64 {
257 return None;
258 }
259 let index = steps.iter().enumerate().map(|(step, row)| (row.id.as_str(), step)).collect();
260 Some(PlaybackPoses { progress, steps, index })
261 }
262
263 pub(crate) fn explode_step_of(&self, id: &str) -> Option<usize> {
265 let active = self.pmi_active_view.as_deref()?;
266 self.pmi_report
267 .as_ref()?
268 .view(active)?
269 .annotations
270 .iter()
271 .filter(|row| row.enabled && row.status == brep_kernel::PmiStatus::Ok)
272 .filter(|row| matches!(row.geometry, PmiGeometry::Explode { .. }))
273 .position(|row| row.id == id)
274 }
275
276 pub fn exploded_progress(&self) -> f64 {
279 self.explode_playback.progress.unwrap_or(self.exploded_step_count() as f64)
280 }
281
282 pub fn exploded_state_json(&self) -> Value {
283 let steps = self.exploded_step_count();
284 json!({
285 "activeView": self.pmi_active_view,
286 "steps": steps,
287 "progress": self.explode_playback.progress.unwrap_or(steps as f64),
288 "animating": self.explode_playback.target.is_some(),
289 "views": self.exploded_views().iter().map(|view| json!({
290 "id": view.id,
291 "name": view.name,
292 "steps": view.steps.iter().map(|step| json!({
293 "id": step.id, "targets": step.targets, "labels": step.labels, "translate": step.translate,
294 "trace": step.trace, "enabled": step.enabled,
295 })).collect::<Vec<_>>(),
296 })).collect::<Vec<_>>(),
297 })
298 }
299
300 pub fn exploded_set_progress(&mut self, progress: f64) {
303 self.explode_playback.target = None;
304 self.set_explode_progress(progress);
305 }
306
307 fn set_explode_progress(&mut self, progress: f64) {
308 let steps = self.exploded_step_count() as f64;
309 self.explode_playback.progress = Some(progress.clamp(0.0, steps));
310 if self.pmi_active_view.is_some() {
311 self.pmi_apply_explode();
312 self.refresh_pmi_overlay();
313 self.sync_transform_gizmo();
315 }
316 }
317
318 pub fn exploded_animate(&mut self, explode: bool, steps_per_second: f64) -> Result<(), String> {
321 if self.pmi_active_view.is_none() {
322 return Err("activate an exploded view first".into());
323 }
324 let steps = self.exploded_step_count() as f64;
325 let to = if explode { steps } else { 0.0 };
326 if self.explode_playback.progress.is_none() {
327 self.set_explode_progress(if explode { 0.0 } else { steps });
329 }
330 self.explode_playback.target = Some((to, steps_per_second.max(1e-3)));
331 Ok(())
332 }
333
334 pub fn exploded_tick(&mut self, dt: f64) -> bool {
337 let Some((to, speed)) = self.explode_playback.target else { return false };
338 if self.pmi_active_view.is_none() {
339 self.explode_playback.target = None;
340 return false;
341 }
342 let from = self.explode_playback.progress.unwrap_or(to);
343 let step = speed * dt.max(0.0);
344 let next = if to > from { (from + step).min(to) } else { (from - step).max(to) };
345 self.set_explode_progress(next);
346 if (next - to).abs() < 1e-12 {
347 self.explode_playback.target = None;
348 return false;
349 }
350 true
351 }
352
353 pub fn selected_occurrences(&self) -> Vec<String> {
359 let selected: std::collections::HashSet<&str> = self.emphasis.selected_solids.iter().map(String::as_str).collect();
360 let mut out: Vec<String> = Vec::new();
361 for name in &self.emphasis.selected_solids {
362 let mut path = String::new();
363 for (at, _) in name.match_indices(':') {
364 path = name[..at].to_string();
365 let prefix = format!("{path}:");
366 if self.scene.solids().iter().filter(|s| s.name.starts_with(&prefix)).all(|s| selected.contains(s.name.as_str())) {
367 break;
368 }
369 }
370 if !path.is_empty() && !out.contains(&path) {
371 out.push(path);
372 }
373 }
374 out
375 }
376
377 pub fn exploded_add_step(&mut self, targets: &[String]) -> Result<String, String> {
382 let view = self
383 .pmi_active_view
384 .clone()
385 .filter(|id| self.is_exploded_view(id))
386 .ok_or("show an exploded view first")?;
387 let targets: Vec<String> = if targets.is_empty() { self.selected_occurrences() } else { targets.to_vec() };
388 if targets.is_empty() {
389 return Err("select the components to move".into());
390 }
391 let mut low = [f64::INFINITY; 3];
392 let mut high = [f64::NEG_INFINITY; 3];
393 for target in &targets {
394 let prefix = format!("{target}:");
395 for solid in self.scene.solids().iter().filter(|s| s.name.starts_with(&prefix)) {
396 for k in 0..3 {
397 low[k] = low[k].min(solid.bbox.min[k]);
398 high[k] = high[k].max(solid.bbox.max[k]);
399 }
400 }
401 }
402 if !low[0].is_finite() {
403 return Err(format!("'{}' has no solids", targets.join(", ")));
404 }
405 let size = (0..3).map(|k| (high[k] - low[k]).powi(2)).sum::<f64>().sqrt().max(1.0);
406 let mut state = self.pmi_state();
407 let step = state.next_id("EXP");
408 state.find_view_mut(&view).ok_or("the exploded view vanished")?.annotations.push(PmiAnnotation {
409 plugin_replay: None,
410 persistent_data: Value::Null,
411 kind: "explode".into(),
412 enabled: true,
413 params: json!({
414 "id": step,
415 "targets": targets,
416 "transform": { "position": [0.0, 0.0, size], "rotationEuler": [0, 0, 0], "scale": [1, 1, 1] },
417 "showTraceLine": true,
418 }),
419 label_world: None,
420 });
421 self.write_pmi_state(state);
422 self.explode_playback = ExplodePlayback::default();
423 self.pmi_set_annotation_open(Some(&step));
424 Ok(step)
425 }
426
427 pub fn assembly_auto_explode(&mut self, name: Option<&str>, spread: f64) -> Result<String, String> {
433 self.assembly_auto_explode_deep(name, spread, 1)
434 }
435
436 fn placements_under(&self, path: Option<&str>) -> Vec<(String, bool)> {
439 let of = |features: &[Value], prefix: &str| -> Vec<(String, bool)> {
440 features
441 .iter()
442 .filter(|f| f["type"] == "ACOMP")
443 .filter_map(|f| {
444 let id = f["inputParams"]["id"].as_str()?;
445 Some((format!("{prefix}{id}"), f["inputParams"]["isFixed"].as_bool().unwrap_or(false)))
446 })
447 .collect()
448 };
449 let Some(path) = path else {
450 return of(self.history.features(), "");
451 };
452 let mut segments = path.split(':');
453 let Some(top) = segments.next() else { return Vec::new() };
454 let Some(params) = self.history.index_of(top).and_then(|i| self.history.feature_params(i)) else { return Vec::new() };
455 let Some(mut part) = params["partName"].as_str().map(str::to_string) else { return Vec::new() };
456 let mut library = self.history.parts_library();
457 let mut document = &library[&part]["document"];
458 for id in segments {
459 let Some(next) = document["features"]
460 .as_array()
461 .and_then(|features| features.iter().find(|f| f["inputParams"]["id"].as_str() == Some(id)))
462 .and_then(|f| f["inputParams"]["partName"].as_str())
463 else {
464 return Vec::new();
465 };
466 part = next.to_string();
467 library = &document["partsLibrary"];
468 document = &library[&part]["document"];
469 }
470 of(document["features"].as_array().map(Vec::as_slice).unwrap_or(&[]), &format!("{path}:"))
471 }
472
473 fn explode_group(&self, placements: &[(String, bool)], spread: f64) -> Option<Vec<(String, f64, [f64; 3])>> {
478 let boxes: Vec<(&str, bool, [f64; 3], [f64; 3])> = placements
479 .iter()
480 .filter_map(|(path, fixed)| {
481 let prefix = format!("{path}:");
482 let mut low = [f64::INFINITY; 3];
483 let mut high = [f64::NEG_INFINITY; 3];
484 for solid in self.scene.solids().iter().filter(|s| s.name.starts_with(&prefix)) {
485 for k in 0..3 {
486 low[k] = low[k].min(solid.bbox.min[k]);
487 high[k] = high[k].max(solid.bbox.max[k]);
488 }
489 }
490 low[0].is_finite().then_some((path.as_str(), *fixed, low, high))
491 })
492 .collect();
493 if boxes.len() < 2 {
494 return None;
495 }
496 let volume = |b: &(&str, bool, [f64; 3], [f64; 3])| (0..3).map(|k| (b.3[k] - b.2[k]).max(0.0)).product::<f64>();
497 let base = boxes
498 .iter()
499 .position(|b| b.1)
500 .unwrap_or_else(|| (0..boxes.len()).max_by(|&a, &b| volume(&boxes[a]).total_cmp(&volume(&boxes[b]))).unwrap_or(0));
501 let centre = |b: &(&str, bool, [f64; 3], [f64; 3])| -> [f64; 3] { std::array::from_fn(|k| (b.2[k] + b.3[k]) / 2.0) };
502 let origin = centre(&boxes[base]);
503 let low: [f64; 3] = std::array::from_fn(|k| boxes.iter().map(|b| b.2[k]).fold(f64::INFINITY, f64::min));
504 let high: [f64; 3] = std::array::from_fn(|k| boxes.iter().map(|b| b.3[k]).fold(f64::NEG_INFINITY, f64::max));
505 let diagonal = (0..3).map(|k| (high[k] - low[k]).powi(2)).sum::<f64>().sqrt().max(1e-9);
506 let distance = spread * diagonal / 2.0;
507 let mut moves: Vec<(String, f64, [f64; 3])> = boxes
508 .iter()
509 .enumerate()
510 .filter(|(i, _)| *i != base)
511 .map(|(_, b)| {
512 let c = centre(b);
513 let d: [f64; 3] = std::array::from_fn(|k| c[k] - origin[k]);
514 let length = d.iter().map(|v| v * v).sum::<f64>().sqrt();
515 let direction = if length > 1e-6 * diagonal { d.map(|v| v / length) } else { [0.0, 0.0, 1.0] };
516 (b.0.to_string(), length, direction.map(|v| v * distance))
517 })
518 .collect();
519 moves.sort_by(|a, b| b.1.total_cmp(&a.1));
521 Some(moves)
522 }
523
524 pub fn assembly_auto_explode_deep(&mut self, name: Option<&str>, spread: f64, depth: usize) -> Result<String, String> {
531 if !(spread.is_finite() && spread > 0.0) {
532 return Err("the spread must be a positive number".into());
533 }
534 self.pmi_deactivate_view();
536 let mut moves: Vec<(String, f64, [f64; 3])> = Vec::new();
537 let mut level: Vec<Option<String>> = vec![None];
538 for _ in 0..depth.max(1) {
539 let mut next = Vec::new();
540 for parent in &level {
541 let placements = self.placements_under(parent.as_deref());
542 if let Some(group) = self.explode_group(&placements, spread) {
543 moves.extend(group);
544 }
545 next.extend(placements.into_iter().map(|(path, _)| Some(path)));
546 }
547 level = next;
548 }
549 if moves.is_empty() {
550 return Err("an exploded view needs at least two placed components".into());
551 }
552 let name = name
553 .map(str::trim)
554 .filter(|n| !n.is_empty())
555 .map(String::from)
556 .unwrap_or_else(|| format!("Exploded view {}", self.exploded_views().len() + 1));
557 let mut exploded = crate::camera::Aabb::empty();
564 for solid in self.scene.solids().iter().filter(|s| s.visible) {
565 let mut shift = [0.0; 3];
566 for (path, _, translate) in &moves {
567 if solid.name.starts_with(path.as_str()) && solid.name[path.len()..].starts_with(':') {
568 for k in 0..3 {
569 shift[k] += translate[k];
570 }
571 }
572 }
573 exploded.expand(std::array::from_fn(|k| solid.bbox.min[k] + shift[k]));
574 exploded.expand(std::array::from_fn(|k| solid.bbox.max[k] + shift[k]));
575 }
576 let camera = self.camera.clone();
577 self.camera.zoom_to_fit(&exploded, 1.15);
578 let id = self.pmi_capture_view(Some(&name));
579 self.camera = camera;
580 let mut state = self.pmi_state();
581 let mut rows = Vec::new();
582 for (component, _, translate) in moves {
583 let step = state.next_id("EXP");
584 rows.push(PmiAnnotation {
585 plugin_replay: None,
586 persistent_data: Value::Null,
587 kind: "explode".into(),
588 enabled: true,
589 params: json!({
590 "id": step,
591 "targets": [component],
592 "transform": { "position": translate, "rotationEuler": [0, 0, 0], "scale": [1, 1, 1] },
593 "showTraceLine": true,
594 }),
595 label_world: None,
596 });
597 }
598 state.find_view_mut(&id).ok_or("the new view vanished")?.annotations.extend(rows);
599 self.write_pmi_state(state);
600 self.explode_playback = ExplodePlayback::default();
601 let _ = self.pmi_activate_view(&id);
602 Ok(id)
603 }
604}
605