1use super::assembly_ops::ComponentInsert;
29use super::EngineState;
30use super::{InstructionsRefField, RefSelectTarget};
31use brep_instructions::model::{Instructions, Target, BLOCK_KEY, DOCUMENT_CLASS};
32use brep_instructions::wizard::Level;
33use brep_instructions::{edit, retarget, source, wizard};
34use brep_kernel::{PmiAnnotation, PmiCamera, PmiDisplay, PmiProjection, PmiView};
35use serde_json::{json, Value};
36
37const GHOST_RGBA: [f32; 4] = [0.62, 0.64, 0.66, 0.28];
39
40const INSERTION_SECONDS: f64 = 0.8;
42const GENERATED_VIEWPORT: [f64; 2] = [1280.0, 800.0];
45
46#[derive(Debug, Clone, PartialEq)]
50pub struct Insertion {
51 view: String,
53 moves: Vec<(Vec<String>, [f64; 3])>,
55 fraction: f64,
57}
58
59#[derive(Debug, Clone, PartialEq)]
61pub enum PlayTo {
62 First,
63 Next,
64 Back,
65 Replay,
67 Step(String),
68}
69
70const DOCUMENT_CLASS_KEY: &str = "documentClass";
72
73impl EngineState {
74 pub fn instructions(&self) -> Result<Option<Instructions>, String> {
76 match self.history.document_block(BLOCK_KEY) {
77 None | Some(Value::Null) => Ok(None),
78 Some(block) => Instructions::from_value(block).map(Some),
79 }
80 }
81
82 pub fn is_instructions(&self) -> bool {
84 self.history.document_block(DOCUMENT_CLASS_KEY).and_then(Value::as_str) == Some(DOCUMENT_CLASS)
85 || self.history.document_block(BLOCK_KEY).is_some()
86 }
87
88 pub(super) fn instructions_required(&self) -> Result<Instructions, String> {
89 self.instructions()?.ok_or_else(|| "this document is not manufacturing instructions".to_string())
90 }
91
92 pub fn instructions_edit<T>(
95 &mut self,
96 coalesce: Option<&str>,
97 edit: impl FnOnce(&mut Instructions) -> Result<T, String>,
98 ) -> Result<T, String> {
99 let mut block = self.instructions_required()?;
100 let out = edit(&mut block)?;
101 block.validate()?;
102 self.history.set_document_block(BLOCK_KEY, Some(block.to_value()), coalesce);
103 Ok(out)
104 }
105
106 pub fn instructions_wrapper(&self) -> Result<(String, String), String> {
108 self.history
109 .features()
110 .iter()
111 .find(|f| super::components::is_acomp_feature_type(f["type"].as_str().unwrap_or_default()))
112 .and_then(|f| Some((f["inputParams"]["id"].as_str()?.to_string(), f["inputParams"]["partName"].as_str()?.to_string())))
113 .ok_or_else(|| "the instructions place no assembly".to_string())
114 }
115
116 pub fn instructions_level(&self) -> Result<Level, String> {
118 let (id, part) = self.instructions_wrapper()?;
119 let document = &self.history.parts_library()[part.as_str()]["document"];
120 if !source::has_placements(document) {
121 return Err(format!("'{part}' is not an assembly: instructions need one to describe"));
122 }
123 Ok(source::level_from_document(document, &id, "", &part))
124 }
125
126 pub fn instructions_start(&mut self, insert: ComponentInsert<'_>, target: Target) -> Result<String, String> {
130 if !self.history.features().is_empty() || self.history.document_block(BLOCK_KEY).is_some() {
131 return Err("instructions start from an empty document".into());
132 }
133 if let ComponentInsert::New { document_json, .. } = &insert {
134 let document: Value = serde_json::from_str(document_json).map_err(|e| format!("the assembly is not a document: {e}"))?;
135 if !source::has_placements(&document) {
136 return Err("instructions need an assembly to describe".into());
137 }
138 }
139 let id = self.insert_component(insert)?;
141 self.history.set_document_block_no_undo(DOCUMENT_CLASS_KEY, Some(Value::String(DOCUMENT_CLASS.into())));
142 self.history.set_document_block_no_undo(BLOCK_KEY, Some(Instructions::new(target).to_value()));
143 Ok(id)
144 }
145
146 pub fn instructions_run_wizard(&mut self) -> Result<usize, String> {
151 let mut block = self.instructions_required()?;
152 let level = self.instructions_level()?;
153 let old_views: Vec<String> = block.steps().filter_map(|(_, s)| s.view.clone()).collect();
154 wizard::propose(&mut block, &level);
155 let mut state = self.pmi_state();
156 state.views.retain(|view| !old_views.contains(&view.id));
157 self.instructions_step_views(&mut block, &mut state)?;
158 block.validate()?;
159 let steps = block.step_count();
160 self.history.set_document_block(BLOCK_KEY, Some(block.to_value()), None);
161 let pmi = if state.is_empty() { None } else { serde_json::to_value(&state).ok() };
162 self.history.set_pmi_block_no_undo(pmi);
163 self.rerun_history();
164 Ok(steps)
165 }
166
167 pub fn instructions_run_wizard_when_built(&mut self) -> Result<(), String> {
172 self.instructions_required()?;
173 if self.run_pending() {
174 self.instructions_wizard_pending = true;
175 return Ok(());
176 }
177 self.instructions_run_wizard().map(|_| ())
178 }
179
180 pub fn instructions_wizard_pending(&self) -> bool {
182 self.instructions_wizard_pending
183 }
184
185 pub fn instructions_restructure<T>(&mut self, edit: impl FnOnce(&mut Instructions) -> Result<T, String>) -> Result<T, String> {
192 self.instructions_restructure_with_views(|block, _| edit(block))
193 }
194
195 fn instructions_restructure_with_views<T>(
198 &mut self,
199 edit: impl FnOnce(&mut Instructions, &mut brep_kernel::PmiState) -> Result<T, String>,
200 ) -> Result<T, String> {
201 let mut block = self.instructions_required()?;
202 let before: Vec<String> = block.steps().filter_map(|(_, s)| s.view.clone()).collect();
203 let mut state = self.pmi_state();
204 let out = edit(&mut block, &mut state)?;
205 block.validate()?;
206 let kept: Vec<String> = block.steps().filter_map(|(_, s)| s.view.clone()).collect();
207 state.views.retain(|view| !before.contains(&view.id) || kept.contains(&view.id));
208 self.instructions_step_views(&mut block, &mut state)?;
209 self.history.set_document_block(BLOCK_KEY, Some(block.to_value()), None);
210 let pmi = if state.is_empty() { None } else { serde_json::to_value(&state).ok() };
211 self.history.set_pmi_block_no_undo(pmi);
212 self.rerun_history();
213 Ok(out)
214 }
215
216 pub(super) fn instructions_fold_retarget(&self, incoming: &mut Value) -> Result<bool, String> {
225 let Ok((id, part)) = self.instructions_wrapper() else { return Ok(false) };
226 let old_entry = &self.history.parts_library()[part.as_str()];
227 let new_entry = &incoming["partsLibrary"][part.as_str()];
228 if new_entry.is_null() || old_entry["document"] == new_entry["document"] {
229 return Ok(false);
230 }
231 let Some(mut block) = incoming.get(BLOCK_KEY).filter(|b| !b.is_null()).map(Instructions::from_value).transpose()? else {
232 return Ok(false);
233 };
234 if block.retarget.is_some() {
235 return Err("the instructions' re-target waits for approval: approve it in the Instructions pane, then update again".into());
236 }
237 let old = source::level_from_document(&old_entry["document"], &id, "", &part);
238 let new = source::level_from_document(&new_entry["document"], &id, "", &part);
239 let text = |key: &str, or: &str| new_entry[key].as_str().filter(|v| !v.is_empty()).unwrap_or(or).to_string();
240 let to = Target {
241 source_key: text("sourceKey", &block.target.source_key),
242 signature: text("sourceSignature", &block.target.signature),
243 ..block.target.clone()
244 };
245 let diff = retarget::diff(&old, &new);
246 let changed = !diff.is_empty();
247 if changed {
248 let before: Vec<String> = block.steps().filter_map(|(_, s)| s.view.clone()).collect();
249 retarget::apply(&mut block, &new, &diff, to)?;
250 let kept: Vec<String> = block.steps().filter_map(|(_, s)| s.view.clone()).collect();
252 if let Some(views) = incoming["pmi"]["views"].as_array_mut() {
253 views.retain(|view| view["id"].as_str().is_none_or(|id| !before.iter().any(|b| b == id) || kept.iter().any(|k| k == id)));
254 }
255 } else {
256 block.target = to;
257 }
258 block.validate()?;
259 incoming[BLOCK_KEY] = block.to_value();
260 Ok(changed)
261 }
262
263 pub(super) fn instructions_refresh_views(&mut self) -> Result<(), String> {
268 let mut block = self.instructions_required()?;
269 let mut state = self.pmi_state();
270 self.instructions_step_views(&mut block, &mut state)?;
271 self.history.set_document_block_no_undo(BLOCK_KEY, Some(block.to_value()));
272 let pmi = if state.is_empty() { None } else { serde_json::to_value(&state).ok() };
273 self.history.set_pmi_block_no_undo(pmi);
274 self.rerun_history();
275 Ok(())
276 }
277
278 pub fn instructions_retarget_approve(&mut self) -> Result<Target, String> {
282 self.instructions_restructure(retarget::approve)
283 }
284
285 pub fn instructions_retarget_reject(&mut self) -> Result<Target, String> {
294 let block = self.instructions_required()?;
295 let from = block.retarget.as_ref().ok_or("no re-target is waiting for approval")?.from.clone();
296 if !self.history.can_undo() {
297 return Err("the re-target came with the file: nothing in this session takes it back; approve it, or update again later".into());
298 }
299 let fold_is_newest = self
300 .history
301 .undo_top_block(BLOCK_KEY)
302 .is_some_and(|before| before.get("retarget").is_none_or(Value::is_null));
303 if !fold_is_newest {
304 return Err("edits followed the re-target: undo them first, or approve it".into());
305 }
306 self.undo();
307 Ok(from)
308 }
309
310 pub fn begin_ref_select_for_instructions(&mut self, step_id: &str, field: InstructionsRefField, seed: Option<&str>) {
316 let restore_index = self.history.rollback();
317 let filter = vec!["SOLID".to_string()];
318 let label = match field {
319 InstructionsRefField::Arrow(_) => "Part the arrow moves".to_string(),
320 InstructionsRefField::Callout(_) => "Part the callout points at".to_string(),
321 };
322 let path = match field {
323 InstructionsRefField::Arrow(index) => vec!["arrows".to_string(), index.to_string(), "occurrence".to_string()],
324 InstructionsRefField::Callout(index) => vec!["callouts".to_string(), index.to_string(), "occurrence".to_string()],
325 };
326 let names = seed.and_then(|occurrence| self.instructions_occurrence_solid(occurrence)).into_iter().collect();
327 self.selection_filter = crate::engine_state::selection_filter::SelectionFilter::from_ref_filter(&filter);
328 self.ref_select = Some(super::RefSelectState {
329 feature_id: step_id.to_string(),
330 path,
331 label,
332 filter,
333 multiple: false,
334 names,
335 restore_index,
336 target: RefSelectTarget::Instructions { field },
337 picks: 0,
338 claimed: false,
339 finish_requested: false,
340 });
341 self.sync_ref_select_emphasis();
342 }
343
344 pub fn instructions_occurrence_of_solid(&self, solid: &str) -> Result<String, String> {
348 let (occurrence, _) = solid.rsplit_once(':').ok_or_else(|| format!("'{solid}' is not a part of the assembly"))?;
349 let level = self.instructions_level()?;
350 if !wizard::flatten(&level).iter().any(|(_, c)| c.path == occurrence) {
351 return Err(format!("'{solid}' is not a part of the assembly"));
352 }
353 Ok(occurrence.to_string())
354 }
355
356 pub fn instructions_occurrence_solid(&self, occurrence: &str) -> Option<String> {
359 let prefix = format!("{occurrence}:");
360 self.scene.solids().iter().map(|solid| solid.name.clone()).find(|name| name.starts_with(&prefix))
361 }
362
363 pub(crate) fn instructions_commit_ref(&mut self, step_id: &str, field: InstructionsRefField, solid: &str) -> Result<(), String> {
368 let occurrence = self.instructions_occurrence_of_solid(solid)?;
369 let step_id = step_id.to_string();
370 self.instructions_edit(None, |block| {
371 let step = block.step_mut(&step_id).ok_or_else(|| format!("no step '{step_id}'"))?;
372 match field {
373 InstructionsRefField::Arrow(index) => {
374 if !step.adds.iter().any(|add| add.occurrences.contains(&occurrence)) {
375 return Err(format!("{occurrence} is not a part this step adds: an arrow moves one of the step's own parts"));
376 }
377 let arrow = step.arrows.get_mut(index).ok_or_else(|| format!("no arrow {index}"))?;
378 arrow.occurrence = occurrence;
379 }
380 InstructionsRefField::Callout(index) => {
381 let callout = step.callouts.get_mut(index).ok_or_else(|| format!("no callout {index}"))?;
382 callout.occurrence = Some(occurrence);
383 }
384 }
385 Ok(())
386 })?;
387 self.history.break_coalescing();
388 Ok(())
389 }
390
391 pub fn instructions_new_step(&mut self, after: Option<&str>) -> Result<String, String> {
394 self.instructions_restructure(|block| edit::insert_step(block, after))
395 }
396
397 pub fn instructions_add_parts(&mut self, step_id: &str, occurrences: &[String]) -> Result<edit::AddReport, String> {
403 let (wrapper, _) = self.instructions_wrapper()?;
404 let level = self.instructions_level()?;
405 let flat = wizard::flatten(&level);
406 let mut components = Vec::new();
407 for occurrence in occurrences {
408 if *occurrence == wrapper {
409 return Err("that row is the whole assembly: drag a part row into the step".into());
410 }
411 let component = flat
412 .iter()
413 .find(|(_, c)| &c.path == occurrence)
414 .map(|(_, c)| *c)
415 .ok_or_else(|| format!("'{occurrence}' is not an occurrence of the assembly"))?;
416 components.push(component);
417 }
418 if self.instructions_required()?.step(step_id).is_none() {
419 return Err(format!("no step '{step_id}'"));
420 }
421 self.instructions_restructure(|block| edit::add_parts(block, step_id, &components))
422 }
423
424 pub fn instructions_add_part(&mut self, step_id: &str, part: &str) -> Result<edit::AddReport, String> {
429 let level = self.instructions_level()?;
430 let occurrences: Vec<String> = level.components.iter().filter(|c| c.part == part || c.part_key() == part).map(|c| c.path.clone()).collect();
431 if occurrences.is_empty() {
432 return Err(format!("the assembly has no part '{part}'"));
433 }
434 self.instructions_add_parts(step_id, &occurrences)
435 }
436
437 pub fn instructions_remove_add(&mut self, step_id: &str, add_index: usize) -> Result<(), String> {
440 self.instructions_restructure(|block| edit::remove_add(block, step_id, add_index).map(drop))
441 }
442
443 pub fn instructions_nest_step(&mut self, step_id: &str, parent: Option<&str>, index: usize) -> Result<(), String> {
447 self.instructions_restructure(|block| edit::nest_step(block, step_id, parent, index))
448 }
449
450 pub fn instructions_unplanned(&self) -> Vec<(String, String)> {
453 let Ok(Some(block)) = self.instructions() else { return Vec::new() };
454 let Ok(level) = self.instructions_level() else { return Vec::new() };
455 edit::unplanned(&block, &level).into_iter().map(|c| (c.path.clone(), c.part.clone())).collect()
456 }
457
458 pub fn instructions_capture_camera(&mut self, step_id: &str) -> Result<(), String> {
462 let snapshot = self.snapshot_camera();
463 self.instructions_restructure_with_views(|block, state| {
464 let step = block.step_mut(step_id).ok_or_else(|| format!("no step '{step_id}'"))?;
465 step.camera_captured = true;
466 if let Some(view) = step.view.as_deref().and_then(|id| state.find_view_mut(id)) {
467 if let Some(section) = view.display.section.as_mut() {
471 section.freeze(view.camera.as_ref());
472 }
473 view.camera = Some(snapshot);
474 }
475 Ok(())
476 })
477 }
478
479 pub fn instructions_reset_view(&mut self, step_id: &str) -> Result<(), String> {
482 self.instructions_restructure(|block| {
483 block.step_mut(step_id).ok_or_else(|| format!("no step '{step_id}'"))?.camera_captured = false;
484 Ok(())
485 })
486 }
487
488 pub(super) fn instructions_step_of_view(&self, view_id: &str) -> Option<String> {
490 let block = self.instructions().ok().flatten()?;
491 let step = block.steps().find(|(_, step)| step.view.as_deref() == Some(view_id)).map(|(_, step)| step.id.clone());
492 step
493 }
494
495 fn instructions_step_views(&mut self, block: &mut Instructions, state: &mut brep_kernel::PmiState) -> Result<(), String> {
505 self.pmi_deactivate_view();
506 let camera = self.camera.clone();
507 let solids: Vec<String> = self.scene.solids().iter().map(|s| s.name.clone()).collect();
508 let boxes: std::collections::HashMap<String, crate::camera::Aabb> =
509 self.scene.solids().iter().map(|s| (s.name.clone(), s.bbox)).collect();
510 fn parents_first(steps: &[brep_instructions::model::Step], out: &mut Vec<String>) {
511 for step in steps {
512 out.push(step.id.clone());
513 parents_first(&step.steps, out);
514 }
515 }
516 let mut ids = Vec::new();
517 parents_first(&block.steps, &mut ids);
518 for step_id in &ids {
519 let presentation = edit::presentation(block, step_id, &solids)?;
520 let number = block.step_number(step_id).unwrap_or(0);
521 let step = block.step(step_id).expect("listed step");
522 let existing = step.view.as_deref().and_then(|id| state.find_view(id)).cloned();
523 let parent_camera = block
524 .parent_id(step_id)
525 .flatten()
526 .and_then(|parent| block.step(parent))
527 .and_then(|parent| parent.view.as_deref())
528 .and_then(|id| state.find_view(id))
529 .and_then(|view| view.camera.clone());
530 let camera_now = match existing.as_ref().and_then(|view| view.camera.clone()).filter(|_| step.camera_captured) {
531 Some(kept) => kept,
532 None => self.generated_step_camera(&presentation, &boxes, parent_camera.as_ref()),
533 };
534 let mut annotations: Vec<PmiAnnotation> = existing
537 .as_ref()
538 .map(|view| {
539 view.annotations.iter().filter(|a| !(a.kind == "balloon" && a.params.get("occurrence").is_some())).cloned().collect()
540 })
541 .unwrap_or_default();
542 let view_id = match &existing {
543 Some(view) => view.id.clone(),
544 None => state.next_id("VIEW"),
545 };
546 for add in step.adds.iter().filter(|a| !a.find_number.is_empty()) {
547 let Some(occurrence) = add.occurrences.first() else { continue };
548 let prefix = format!("{occurrence}:");
549 let Some(member) = solids.iter().find(|name| name.starts_with(&prefix)) else { continue };
550 annotations.push(PmiAnnotation {
551 plugin_replay: None,
552 persistent_data: Value::Null,
553 kind: "balloon".into(),
554 enabled: true,
555 params: json!({ "id": state.next_id("BAL"), "target": member, "occurrence": occurrence }),
556 label_world: None,
557 });
558 }
559 let display = PmiDisplay {
560 hidden: presentation.hidden,
561 ..existing.as_ref().map(|view| view.display.clone()).unwrap_or(PmiDisplay {
562 text_size_pt: 12.0,
563 wireframe: false,
564 hidden: Vec::new(),
565 section: None,
566 })
567 };
568 let view = PmiView {
569 id: view_id.clone(),
570 name: format!("Step {number} · {}", step.title),
571 camera: Some(camera_now),
572 display,
573 annotations,
574 };
575 match state.find_view_mut(&view_id) {
576 Some(slot) => *slot = view,
577 None => state.views.push(view),
578 }
579 block.step_mut(step_id).expect("listed step").view = Some(view_id);
580 }
581 self.camera = camera;
582 Ok(())
583 }
584
585 fn generated_step_camera(
594 &mut self,
595 presentation: &edit::Presentation,
596 boxes: &std::collections::HashMap<String, crate::camera::Aabb>,
597 parent: Option<&PmiCamera>,
598 ) -> PmiCamera {
599 let union = |names: &[String]| {
600 let mut bbox = crate::camera::Aabb::empty();
601 for name in names {
602 if let Some(solid) = boxes.get(name) {
603 bbox.union(solid);
604 }
605 }
606 bbox
607 };
608 let mut shown = union(&presentation.placed);
609 shown.union(&union(&presentation.adding));
610 let frame = shown.clone();
611 match parent {
612 Some(parent) => {
613 let (kind, scale) = match parent.projection {
614 PmiProjection::Orthographic { half_height } => ("orthographic", half_height),
615 PmiProjection::Perspective { fov_y_deg } => ("perspective", fov_y_deg),
616 };
617 let json = json!({ "kind": kind, "eye": parent.eye, "target": parent.target, "up": parent.up, "scale": scale }).to_string();
618 let _ = self.camera.apply_state_json(&json);
619 }
620 None => {
621 self.camera.projection = crate::view::Projection::Orthographic { half_height: 10.0 };
624 self.camera.standard_view("iso");
625 }
626 }
627 self.camera.width = GENERATED_VIEWPORT[0];
628 self.camera.height = GENERATED_VIEWPORT[1];
629 if !frame.is_empty() {
630 self.camera.zoom_to_fit(&frame, 1.25);
631 }
632 if matches!(self.camera.projection, crate::view::Projection::Orthographic { .. }) {
636 let size = shown.size();
637 let diagonal = (size[0] * size[0] + size[1] * size[1] + size[2] * size[2]).sqrt();
638 let distance = (4.0 * diagonal).max(10.0);
639 let (eye, target) = (self.camera.eye, self.camera.target);
640 let direction = [eye[0] - target[0], eye[1] - target[1], eye[2] - target[2]];
641 let length = (direction[0] * direction[0] + direction[1] * direction[1] + direction[2] * direction[2]).sqrt();
642 if length > 1e-12 {
643 self.camera.eye = [
644 target[0] + direction[0] / length * distance,
645 target[1] + direction[1] / length * distance,
646 target[2] + direction[2] / length * distance,
647 ];
648 }
649 }
650 self.snapshot_camera()
651 }
652
653 pub fn instructions_show_step(&mut self, step_id: &str) -> Result<(), String> {
655 let block = self.instructions_required()?;
656 let step = block.step(step_id).ok_or_else(|| format!("no step '{step_id}'"))?;
657 let view = step.view.clone().ok_or_else(|| format!("step '{step_id}' has no view: run the wizard or pick one"))?;
658 self.pmi_activate_view(&view)
659 }
660
661 pub(crate) fn instructions_refresh_ghosts(&mut self) {
670 if !self.is_instructions() {
671 return;
672 }
673 let placed = self.instructions_shown_presentation().map(|presentation| presentation.placed).unwrap_or_default();
674 let gone: Vec<String> =
675 self.context_scene.solids().iter().map(|ghost| ghost.name.clone()).filter(|name| !placed.contains(name)).collect();
676 for name in gone {
677 self.context_scene.remove_solid(&name);
678 self.scene.set_visible(&name, true);
679 self.dirty = true;
680 }
681 for name in placed {
682 let Some(solid) = self.scene.solid(&name) else { continue };
683 if !solid.visible && self.context_scene.solid(&name).is_none() {
686 continue;
687 }
688 let mut ghost = solid.clone();
689 ghost.visible = true;
690 ghost.source_handle = 0;
691 ghost.vertices.clear();
692 if self.context_scene.solid(&name) != Some(&ghost) {
693 self.context_scene.insert_solid(ghost);
694 self.dirty = true;
695 }
696 if solid.visible {
697 self.scene.set_visible(&name, false);
698 self.dirty = true;
699 }
700 }
701 if !self.context_scene.solids().is_empty() && self.context_scene.tint != Some(GHOST_RGBA) {
702 self.context_scene.tint = Some(GHOST_RGBA);
703 self.dirty = true;
704 }
705 }
706
707 pub fn instructions_play(&mut self, to: PlayTo) -> Result<String, String> {
712 let block = self.instructions_required()?;
713 let order: Vec<String> = block.steps().map(|(_, step)| step.id.clone()).collect();
714 if order.is_empty() {
715 return Err("there are no steps yet: run the wizard first".into());
716 }
717 let shown = self.instructions_shown_step();
718 let at = shown.as_ref().and_then(|id| order.iter().position(|step| step == id));
719 let step = match to {
720 PlayTo::First => order[0].clone(),
721 PlayTo::Next => order[at.map_or(0, |i| (i + 1).min(order.len() - 1))].clone(),
722 PlayTo::Back => order[at.map_or(0, |i| i.saturating_sub(1))].clone(),
723 PlayTo::Replay => shown.unwrap_or_else(|| order[0].clone()),
724 PlayTo::Step(id) => order.iter().find(|step| **step == id).cloned().ok_or_else(|| format!("no step '{id}'"))?,
725 };
726 self.instructions_show_step(&step)?;
727 let view = self.pmi_active_view().map(str::to_string).unwrap_or_default();
728 let solids: Vec<String> = self.scene.solids().iter().map(|solid| solid.name.clone()).collect();
729 let presentation = edit::presentation(&block, &step, &solids)?;
730 let under = |path: &str, occurrence: &str| path == occurrence || (path.starts_with(occurrence) && path[occurrence.len()..].starts_with(':'));
731 let moves: Vec<(Vec<String>, [f64; 3])> = block
732 .step(&step)
733 .map(|found| {
734 found
735 .arrows
736 .iter()
737 .map(|arrow| {
738 let names: Vec<String> = presentation.adding.iter().filter(|name| under(name, &arrow.occurrence)).cloned().collect();
741 (names, arrow.start_offset())
742 })
743 .filter(|(names, offset)| !names.is_empty() && offset.iter().any(|v| *v != 0.0))
744 .collect()
745 })
746 .unwrap_or_default();
747 self.instructions_insertion = (!moves.is_empty()).then(|| Insertion { view, moves, fraction: 0.0 });
748 self.pmi_apply_explode();
749 self.refresh_pmi_overlay();
750 Ok(step)
751 }
752
753 pub fn instructions_insertion_tick(&mut self, dt: f64) -> bool {
756 let Some(insertion) = self.instructions_insertion.as_mut() else { return false };
757 if self.pmi_active_view.as_deref() != Some(insertion.view.as_str()) {
758 self.instructions_insertion = None;
759 return false;
760 }
761 insertion.fraction = (insertion.fraction + dt.max(0.0) / INSERTION_SECONDS).min(1.0);
762 let done = insertion.fraction >= 1.0;
763 if done {
764 self.instructions_insertion = None;
765 }
766 self.pmi_apply_explode();
767 self.refresh_pmi_overlay();
768 !done
769 }
770
771 pub(crate) fn instructions_insertion_poses(&self) -> Vec<(Vec<String>, [f64; 3], [f64; 3], [f64; 3], [f64; 3])> {
775 let Some(insertion) = &self.instructions_insertion else { return Vec::new() };
776 if self.pmi_active_view.as_deref() != Some(insertion.view.as_str()) {
777 return Vec::new();
778 }
779 let left = 1.0 - insertion.fraction;
780 insertion
781 .moves
782 .iter()
783 .map(|(names, offset)| (names.clone(), offset.map(|v| v * left), [0.0; 3], [1.0; 3], [0.0; 3]))
784 .collect()
785 }
786
787 pub fn instructions_insertion_fraction(&self) -> Option<f64> {
789 self.instructions_insertion.as_ref().map(|insertion| insertion.fraction)
790 }
791
792 pub fn instructions_shown_step(&self) -> Option<String> {
794 let active = self.pmi_active_view()?;
795 let block = self.instructions().ok().flatten()?;
796 let step = block.steps().find(|(_, step)| step.view.as_deref() == Some(active)).map(|(_, step)| step.id.clone());
797 step
798 }
799
800 fn instructions_shown_presentation(&self) -> Option<edit::Presentation> {
803 let active = self.pmi_active_view()?;
804 let block = self.instructions().ok().flatten()?;
805 let (_, step) = block.steps().find(|(_, step)| step.view.as_deref() == Some(active))?;
806 let solids: Vec<String> = self.scene.solids().iter().map(|solid| solid.name.clone()).collect();
807 edit::presentation(&block, &step.id, &solids).ok()
808 }
809
810 pub fn instructions_state_json(&self) -> Value {
813 match self.instructions() {
814 Ok(Some(block)) => {
815 let shown = self.pmi_active_view().and_then(|view| {
816 block.steps().find(|(_, s)| s.view.as_deref() == Some(view)).map(|(_, s)| s.id.clone())
817 });
818 json!({
819 "instructions": block.to_value(),
820 "steps": block.step_count(),
821 "wizardPending": self.instructions_wizard_pending,
822 "viewsPending": self.instructions_views_pending,
823 "shownStep": shown,
824 "ghosted": self.context_scene.solids().iter().map(|ghost| ghost.name.as_str()).collect::<Vec<_>>(),
825 "insertion": self.instructions_insertion_fraction(),
826 "emptySteps": edit::empty_steps(&block),
827 "unplanned": self.instructions_unplanned().into_iter().map(|(occurrence, part)| json!({ "occurrence": occurrence, "part": part })).collect::<Vec<_>>(),
828 "wrapper": self.instructions_wrapper().ok().map(|(id, part)| json!({ "id": id, "part": part })),
829 })
830 }
831 Ok(None) => json!({ "instructions": null }),
832 Err(error) => json!({ "instructions": null, "error": error }),
833 }
834 }
835}