Skip to main content

brep_render/engine_state/
instructions_motion.rs

1//! A motion study's published motion on a manufacturing-instructions step.
2//!
3//! The assembly the instructions describe is placed as the grounded wrapper
4//! with its document embedded ([`EngineState::instructions_level`] reads it);
5//! a study the Simulation workbench published there carries its `motion`
6//! (the player's `Motion` JSON with `study` and `assemblySignature` beside
7//! the frames). A step takes a copy of it as its own `motion`
8//! (`edit::update_step`), so the instructions play without the assembly at
9//! hand and the player ([`EngineState::instructions_player_data`]) re-keys it
10//! under the wrapper. A motion published before the assembly changed is
11//! refused: its signature no longer matches the embedded document's.
12
13use super::simulation_player::assembly_signature;
14use super::EngineState;
15use brep_instructions::edit;
16use brep_simulation::model::{Simulation, BLOCK_KEY};
17use serde_json::{json, Value};
18
19/// A study of the assembly whose published motion a step can take.
20#[derive(Debug, Clone, PartialEq, serde::Serialize)]
21#[serde(rename_all = "camelCase")]
22pub struct PublishedMotion {
23    pub study: String,
24    pub name: String,
25    pub frames: usize,
26    pub duration: f64,
27    pub fps: u32,
28    /// Whether the motion was published from the assembly as the
29    /// instructions hold it (a stale one cannot be attached).
30    pub current: bool,
31}
32
33impl EngineState {
34    /// The wrapper's embedded document and its signature.
35    fn instructions_target_document(&self) -> Result<(Value, String), String> {
36        let (_, part) = self.instructions_wrapper()?;
37        let document = self.history.parts_library()[part.as_str()]["document"].clone();
38        if document.is_null() {
39            return Err(format!("'{part}' has no embedded document"));
40        }
41        let signature = assembly_signature(&document);
42        Ok((document, signature))
43    }
44
45    /// The assembly's studies that carry a published motion, in block order.
46    pub fn instructions_published_motions(&self) -> Result<Vec<PublishedMotion>, String> {
47        let (document, signature) = self.instructions_target_document()?;
48        let block = match document.get(BLOCK_KEY) {
49            None | Some(Value::Null) => return Ok(Vec::new()),
50            Some(block) => Simulation::from_value(block)?,
51        };
52        Ok(block
53            .studies
54            .iter()
55            .filter_map(|study| {
56                let motion = study.motion.as_ref()?;
57                Some(PublishedMotion {
58                    study: study.id.clone(),
59                    name: study.name.clone(),
60                    frames: motion.get("frames").and_then(Value::as_array).map_or(0, Vec::len),
61                    duration: motion.get("duration").and_then(Value::as_f64).unwrap_or(study.duration),
62                    fps: motion.get("fps").and_then(Value::as_u64).map_or(study.fps, |f| f as u32),
63                    current: motion.get("assemblySignature").and_then(Value::as_str) == Some(signature.as_str()),
64                })
65            })
66            .collect())
67    }
68
69    /// Attach the published motion of `study` (the only published one when
70    /// `None`) to `step` as its motion. Refused when the study has none, when
71    /// it was published before the assembly changed (publish it again there),
72    /// or when several are published and none is named. One undo step.
73    /// Returns the study id.
74    pub fn instructions_attach_motion(&mut self, step: &str, study: Option<&str>) -> Result<String, String> {
75        let (document, signature) = self.instructions_target_document()?;
76        let block = match document.get(BLOCK_KEY) {
77            None | Some(Value::Null) => return Err("the assembly has no motion study: publish one in the Simulation workbench".into()),
78            Some(block) => Simulation::from_value(block)?,
79        };
80        let published: Vec<_> = block.studies.iter().filter(|s| s.motion.is_some()).collect();
81        let study = match study {
82            Some(id) => block.study(id).ok_or_else(|| format!("the assembly has no study '{id}'"))?,
83            None => match published.as_slice() {
84                [one] => one,
85                [] => return Err("the assembly has no published motion: publish a study in the Simulation workbench".into()),
86                _ => return Err(format!("the assembly has {} published motions: name the study", published.len())),
87            },
88        };
89        let motion = study.motion.as_ref().ok_or_else(|| format!("study '{}' is not published: publish it in the Simulation workbench", study.name))?;
90        if motion.get("assemblySignature").and_then(Value::as_str) != Some(signature.as_str()) {
91            return Err(format!("study '{}' was published before the assembly changed: publish it again there", study.name));
92        }
93        if motion.get("frames").and_then(Value::as_array).is_none_or(Vec::is_empty) {
94            return Err(format!("study '{}' published no frames", study.name));
95        }
96        let id = study.id.clone();
97        let patch = json!({ "motion": motion });
98        self.instructions_edit(None, |block| edit::update_step(block, step, &patch))?;
99        Ok(id)
100    }
101
102    /// Take the motion off `step`; the insertion animation plays again.
103    pub fn instructions_detach_motion(&mut self, step: &str) -> Result<(), String> {
104        self.instructions_edit(None, |block| edit::update_step(block, step, &json!({ "motion": null })))
105    }
106}