Skip to main content

brep_render/engine_state/
instructions_ops.rs

1//! Manufacturing instructions on the engine: an instructions document places
2//! its target assembly as one grounded component (the WRAPPER) and keeps what
3//! the author writes in its `instructions` block (`brep_instructions::model`).
4//!
5//! What this module adds on top of the crate:
6//! * [`EngineState::instructions_start`]: turn an empty document into
7//!   instructions for an assembly;
8//! * [`EngineState::instructions_level`]: the wrapper's assembly as the
9//!   wizard's occurrence tree, read from its embedded document;
10//! * [`EngineState::instructions_run_wizard`]: propose the step tree and
11//!   give each step its own PMI view: an iso camera fitted to what the step
12//!   shows, the parts not placed yet hidden, and a balloon on each added
13//!   part that has a Find Number (a placement inside the wrapper, so the
14//!   balloon names its occurrence);
15//! * [`EngineState::instructions_restructure`]: an edit that changes which
16//!   step adds what (merge, split, insert, reorder, move a part, delete), with
17//!   the step views brought up to date in the same undo step;
18//! * [`EngineState::instructions_edit`]: every other edit, read-modify-write
19//!   of the block as one undo step;
20//! * [`EngineState::instructions_show_step`]: show a step's view.
21//! * [`EngineState::instructions_fold_retarget`]: a new state of the
22//!   assembly re-targets the steps in the same undo step, waiting for
23//!   [`EngineState::instructions_retarget_approve`].
24//!
25//! The wizard reads the scene, so right after a start it waits for the run
26//! that builds the wrapper ([`EngineState::instructions_run_wizard_when_built`]).
27
28use 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
37/// The ghost of a part an earlier step placed: neutral grey, see-through.
38const GHOST_RGBA: [f32; 4] = [0.62, 0.64, 0.66, 0.28];
39
40/// How long a step's parts take to come home along their insertion arrows.
41const INSERTION_SECONDS: f64 = 0.8;
42/// The viewport a generated step camera is fitted in (the player data's
43/// default), so the fit does not depend on the window the edit was made in.
44const GENERATED_VIEWPORT: [f64; 2] = [1280.0, 800.0];
45
46/// A step's insertion motion in the in-app player: its arrowed parts start
47/// out along their arrows and move home. Display poses only (the explode
48/// machinery's), never a write to the document.
49#[derive(Debug, Clone, PartialEq)]
50pub struct Insertion {
51    /// The view it plays in; showing any other puts it away.
52    view: String,
53    /// The solids under each arrow and the arrow's start offset.
54    moves: Vec<(Vec<String>, [f64; 3])>,
55    /// 0 = at the arrows' start, 1 = home.
56    fraction: f64,
57}
58
59/// Where the player goes.
60#[derive(Debug, Clone, PartialEq)]
61pub enum PlayTo {
62    First,
63    Next,
64    Back,
65    /// The shown step again, its insertion from the start.
66    Replay,
67    Step(String),
68}
69
70/// The document key of the document class (`documentClass`).
71const DOCUMENT_CLASS_KEY: &str = "documentClass";
72
73impl EngineState {
74    /// The document's instructions block; `Ok(None)` when it has none.
75    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    /// Whether the document is an instructions document.
83    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    /// Read the block, apply `edit`, validate and write it back as ONE undo
93    /// step (`coalesce` groups a typing run). Nothing is written on a refusal.
94    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    /// The wrapper component: `(id, part name)` of the first placement.
107    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    /// The wrapper's assembly as the wizard's occurrence tree.
117    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    /// Make this EMPTY document the instructions for an assembly: place it as
127    /// the grounded wrapper, mark the document class and start a block for
128    /// `target`. One undo step. Returns the wrapper's id.
129    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        // The first placement is inserted grounded.
140        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    /// Propose the step tree from the wrapper's assembly, replacing the
147    /// block's steps (the target, template and front matter stay), and
148    /// give each step its view. The views the old steps had are removed. One
149    /// undo step. Returns the step count.
150    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    /// [`instructions_run_wizard`](Self::instructions_run_wizard) now when
168    /// the scene is current, else once the pending run lands (a background
169    /// runner has not built the wrapper's solids yet right after
170    /// [`instructions_start`](Self::instructions_start)).
171    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    /// Whether the wizard is waiting for a run to land.
181    pub fn instructions_wizard_pending(&self) -> bool {
182        self.instructions_wizard_pending
183    }
184
185    /// A structural edit (merge, split, insert, reorder, move a part, delete):
186    /// `edit` on the block, then every step's view brought up to date: the
187    /// parts hidden and the balloons follow the steps' parts, each step keeps
188    /// its camera and the author's own annotations, a step without a view
189    /// gets one, and the views of steps that went away are removed. One undo
190    /// step.
191    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    /// [`Self::instructions_restructure`] whose edit also reaches the PMI
196    /// state (a step's view camera).
197    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    /// Fold a new state of the wrapper's assembly into the steps of the
217    /// `incoming` document before it is adopted (Update components, a linked
218    /// tab's newer state), so the assembly and the re-target are one undo
219    /// step: the wizard reruns on the changed parts only and the change waits
220    /// in the block's `retarget` for the author's approval. An assembly whose
221    /// occurrences did not change only moves the target. Refused while a
222    /// re-target waits. Returns whether the steps changed (their views are
223    /// made once the new revision is built).
224    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            // The views of steps a dropped sub-assembly's group took away.
251            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    /// Bring every step's view up to date with the block without an undo step
264    /// of its own (it belongs to the edit that changed the steps): what each
265    /// hides and its balloons follow the steps' parts, and a new step gets a
266    /// view fitted to the solids the run just built.
267    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    /// Approve the pending re-target: the new revision becomes the target and
279    /// the steps it left empty go, with their views. One undo step. Returns
280    /// the new target.
281    pub fn instructions_retarget_approve(&mut self) -> Result<Target, String> {
282        self.instructions_restructure(retarget::approve)
283    }
284
285    /// Reject the pending re-target: the ONE undo that takes the new revision
286    /// and its re-target back together (what Undo does), offered as a button
287    /// and a command. Refused when none waits; when the re-target came in with
288    /// the file (nothing in this session takes it back: approve it, or update
289    /// again later); and when a later edit sits on top of that undo step (it
290    /// would go too: undo it first, or approve). The newest step is the
291    /// re-target's when the block as it stood before it has no `retarget`.
292    /// Returns the target the instructions keep describing.
293    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    /// Enter reference selection for step `step_id`'s occurrence `field` (an
311    /// arrow's part or a callout's): the field takes ONE solid of the placed
312    /// assembly, picked in the viewport, and Finish derives its occurrence
313    /// (`Self::instructions_commit_ref`). `seed` is the occurrence it holds
314    /// now, lit as the pick's seed through its first solid. Nothing rolls.
315    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    /// The occurrence a picked solid belongs to: its name without the member
345    /// (`ACOMP1:ACOMP3:Pin` → `ACOMP1:ACOMP3`), when the placed assembly
346    /// holds that occurrence.
347    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    /// The first drawn solid of `occurrence` (`<occurrence>:<member>`), for
357    /// lighting the occurrence in the viewport.
358    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    /// Write a finished occurrence pick into step `step_id`'s `field`: an
364    /// arrow takes a part the step adds (any other is refused, the arrow
365    /// unchanged), a callout any part of the assembly. One undo step, the
366    /// step views following.
367    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    /// A new empty step after `after` (or at the end), with its own view.
392    /// Returns its id.
393    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    /// A BOM row dragged into a step: add `occurrences` of the wrapper's
398    /// assembly to `step_id` ([`edit::add_parts`]). An occurrence not in the
399    /// assembly, or a step that does not exist, is refused with nothing
400    /// written. The step views follow in the same undo step. Returns what
401    /// was added and what was left where it is.
402    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    /// Every occurrence of the part named `part` (its name or its PLM part id) at the assembly's top level
425    /// (what a packed BOM row stands for; a sub-assembly's own parts belong
426    /// to its group) into `step_id`, the ones already in a step reported.
427    /// Refused for a part the top level does not have.
428    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    /// Take a part out of a step ([`edit::remove_add`]): it is not planned
438    /// any more until it is added again. One undo step, views following.
439    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    /// Move a step under `parent` (`None`: the top level) at `index`, its
444    /// children with it ([`edit::nest_step`]). One undo step, views following
445    /// (a nested step's camera looks from its new parent's direction).
446    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    /// The parts of the assembly not yet in any step, as `(occurrence, part)`
451    /// ([`edit::unplanned`]); empty when the document is not instructions.
452    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    /// Capture the viewport's camera as `step_id`'s view camera. It is the
459    /// author's from then on: every edit keeps it, until
460    /// [`Self::instructions_reset_view`]. One undo step.
461    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                // A section saved before its plane was stored is anchored on
468                // the old camera first, as the PMI workbench's Update camera
469                // does (no step view carries a section today).
470                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    /// Drop `step_id`'s captured camera: its view's camera is generated again
480    /// from the step's parts and its parent's direction. One undo step.
481    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    /// The step whose view `view_id` is, if any.
489    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    /// Give every step of `block` its view in `state`: the parts not placed
496    /// yet hidden and a balloon on each added part that has a Find Number (a
497    /// placement inside the wrapper, so the balloon names its occurrence).
498    /// A step's existing view keeps its id and the annotations the author
499    /// added (explode poses among them). Its camera is generated from the
500    /// step's parts ([`Self::generated_step_camera`]) unless the author
501    /// captured one, which stays until Reset view. Parents go before their
502    /// children, whose cameras look from the parent's direction. The user's
503    /// camera is put back afterwards.
504    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            // The author's annotations stay; the step's own balloons (the
535            // ones naming an occurrence) are made again from its parts.
536            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    /// A step's generated camera: it frames everything the step shows, the
586    /// parts it adds (its own and its children's) and the ghosts placed
587    /// before it, so no part runs off the picture. It looks from its parent's
588    /// camera direction, with the parent's up and projection, or from the
589    /// isometric in the app's default (orthographic) projection at the top
590    /// level. The fit is made in a fixed viewport
591    /// ([`GENERATED_VIEWPORT`]), so the same step gives the same camera
592    /// whatever window the edit was made in. Leaves `self.camera` changed.
593    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                // The app's default projection, whatever the viewport is in,
622                // so the same step gives the same camera.
623                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        // An orthographic fit keeps the eye where it was: put it at a fixed
633        // distance (clear of everything shown) so it depends on the frame
634        // alone, not on the viewport the edit was made in.
635        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    /// Show a step's view (its camera, hidden parts and balloons).
654    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    /// Draw the shown step as the players do: the parts earlier steps placed
662    /// ghosted (grey and translucent, in the context layer, which draws
663    /// without depth writes or picks) and the parts the step adds solid. Run
664    /// after a view is applied and after every run while one is shown (the
665    /// copies follow the rebuilt displays); with no step shown, or before a
666    /// view is put away, the ghosts go and their parts show again. Only an
667    /// instructions document's context layer is touched: a Work part keeps
668    /// its context on the part's own engine.
669    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            // A hidden part without a ghost is one the view hides; one with a
684            // ghost was ghosted before, and a run may have rebuilt it since.
685            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    /// Play a step in the in-app player: show its view (its camera, ghosts
708    /// and balloons) and start its parts' insertion along their arrows.
709    /// Steps go in reading order (a step's children before it); Next on
710    /// the last step and Back on the first stay put. Returns the step shown.
711    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                        // Only the parts this step adds move: a placed part's
739                        // display is a ghost's source and stays where it is.
740                        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    /// Advance the insertion by `dt` seconds; `true` while another frame is
754    /// needed. The app calls it once per frame.
755    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    /// The insertion's display poses for [`Self::pmi_apply_explode`]:
772    /// `(solids, translate, rotate°, scale, centre)`, empty unless the view
773    /// it plays in is the one shown.
774    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    /// How far the shown step's parts have come home (0..1), while they move.
788    pub fn instructions_insertion_fraction(&self) -> Option<f64> {
789        self.instructions_insertion.as_ref().map(|insertion| insertion.fraction)
790    }
791
792    /// The step whose view is shown.
793    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    /// The presentation of the step whose view is shown, over the scene's
801    /// solids.
802    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    /// What automation and the panes read: the block, the step being shown,
811    /// the steps left empty and the step count.
812    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}