Skip to main content

brep_render/engine_state/
simulation_player.rs

1//! A motion study for the player: the baked frames as the player template's
2//! `Motion` (per frame, the moved occurrences' pose deltas, the hidden ones,
3//! opacities, the camera a camera clip drives, the clashing pairs), the
4//! scene's meshes and nodes as the document poses them, and the offline
5//! `.html` — the same trusted template the manufacturing instructions use
6//! (`brep_instructions::player`), with no steps and a timeline instead.
7//!
8//! Nothing here needs a GPU: the web app makes the page too.
9
10use super::simulation_play::{mat_mul, rigid_inverse, same_matrix, IDENTITY};
11use super::EngineState;
12use brep_instructions::model::Target;
13use brep_instructions::player::{self, Camera, Mode, Motion, MotionFrame, PlayerData, PlayerMesh, PlayerNode, Scene};
14use brep_kernel::{PmiCamera, PmiProjection};
15use brep_simulation::model::BLOCK_KEY;
16use serde_json::{json, Value};
17use std::collections::BTreeMap;
18
19/// The signature of an assembly document a published motion was made from:
20/// the document's content hash with every study's published motion left out,
21/// so it reads the same before and after a publish and differs once anything
22/// else in the document (parts, mates, a study) changes.
23pub fn assembly_signature(document: &Value) -> String {
24    let mut stripped = document.clone();
25    if let Some(studies) = stripped.get_mut(BLOCK_KEY).and_then(|block| block.get_mut("studies")).and_then(Value::as_array_mut) {
26        for study in studies {
27            if let Some(object) = study.as_object_mut() {
28                object.remove("motion");
29            }
30        }
31    }
32    super::assembly_ops::document_signature(&stripped.to_string())
33}
34
35/// Pose deltas and opacities are written to this many decimals.
36fn rounded(value: f64) -> f64 {
37    (value * 1e4).round() / 1e4
38}
39
40/// A row-major 4×4 as the player multiplies it (column-major).
41fn column_major(m: &[f64; 16]) -> [f64; 16] {
42    std::array::from_fn(|k| {
43        let (col, row) = (k / 4, k % 4);
44        rounded(m[row * 4 + col])
45    })
46}
47
48fn player_camera(camera: &PmiCamera) -> Camera {
49    let (fov_deg, half_height) = match camera.projection {
50        PmiProjection::Perspective { fov_y_deg } => (Some(fov_y_deg), None),
51        PmiProjection::Orthographic { half_height } => (None, Some(half_height)),
52    };
53    Camera { eye: camera.eye, target: camera.target, up: camera.up, fov_deg, half_height }
54}
55
56impl EngineState {
57    /// The study (the active one when `None`) baked to the end and read out
58    /// frame by frame as the player's motion.
59    pub fn simulation_motion(&mut self, study: Option<&str>) -> Result<Motion, String> {
60        self.simulation_bake_all(study)?;
61        self.motion_from_playback()
62    }
63
64    /// Publish the study (the active one when `None`): bake it to the end
65    /// and write its motion into the document's `simulation` block as the
66    /// study's `motion` — the player's `Motion` JSON with `study` and
67    /// `assemblySignature` ([`assembly_signature`] of this document) beside
68    /// the frames — for a manufacturing-instructions step to attach. One
69    /// undo step; the bake is kept. Returns the study id and the frame count.
70    pub fn simulation_publish(&mut self, study: Option<&str>) -> Result<(String, usize), String> {
71        let id = self.simulation_bake_all(study)?;
72        let motion = self.motion_from_playback()?;
73        let frames = motion.frames.len();
74        let document: Value = serde_json::from_str(&self.history.request_json()).map_err(|e| format!("the document does not read back: {e}"))?;
75        let signature = assembly_signature(&document);
76        let mut value = serde_json::to_value(&motion).expect("a motion serializes");
77        value["study"] = json!(id);
78        value["assemblySignature"] = json!(signature);
79        let written = value.clone();
80        self.simulation_edit(None, |block| {
81            let study = block.study_mut(&id).ok_or_else(|| format!("no study '{id}'"))?;
82            study.motion = Some(value);
83            Ok(())
84        })?;
85        self.simulation_keep_bake_through_publish(&id, written);
86        Ok((id, frames))
87    }
88
89    /// The player data of the study as its bake stands — no baking, no
90    /// posing, nothing touched: what a PLM save publishes without disturbing
91    /// the scene. `Err` until a study has been played or scrubbed and its
92    /// bake has finished.
93    pub fn simulation_player_data_baked(&self) -> Result<PlayerData, String> {
94        let play = self.simulation_playback().ok_or("no study has been played or scrubbed yet")?;
95        let total = play.baked_study().frame_count();
96        if play.frames.len() < total {
97            return Err(format!("the study's bake is not finished ({} of {total} frames): let it play once", play.frames.len()));
98        }
99        // A document edit since the bake (a mate, a part, a study, a PMI view
100        // an explode clip plays) makes the frames another assembly's: the
101        // next play re-bakes; until then nothing is published.
102        if play.revision() != self.history.revision() {
103            return Err("the study or the assembly changed since its bake: play it again".into());
104        }
105        self.player_data_from_playback()
106    }
107
108    /// The playback's frames as the player's motion (the bake as it stands).
109    fn motion_from_playback(&self) -> Result<Motion, String> {
110        let play = self.simulation_playback().ok_or("no study to publish")?;
111        let record = play.baked_study().clone();
112        let base = play.base().clone();
113        let count = play.frames.len();
114        let mut frames = Vec::with_capacity(count);
115        for index in 0..count {
116            let time = record.frame_time(index);
117            let sample = record.sample(time, &|id| base.get(id).copied());
118            let worlds = self.simulation_frame_worlds(index).unwrap_or_default();
119            let mut poses = BTreeMap::new();
120            for (occurrence, world) in &worlds {
121                let Some(document) = self.occurrence_document_matrix(occurrence) else { continue };
122                let delta = mat_mul(world, &rigid_inverse(&document));
123                if !same_matrix(&delta, &IDENTITY) {
124                    poses.insert(occurrence.clone(), column_major(&delta));
125                }
126            }
127            let camera = sample.camera.as_ref().and_then(|c| self.simulation_camera_between(&c.from, &c.to, c.t)).map(|c| player_camera(&c));
128            let mut clashing: Vec<String> = Vec::new();
129            if let Some(frame) = self.simulation_playback().and_then(|play| play.frames.get(index)) {
130                for (a, b) in &frame.clashes {
131                    for id in [a, b] {
132                        if !clashing.contains(id) {
133                            clashing.push(id.clone());
134                        }
135                    }
136                }
137            }
138            let opacity = sample.opacity.iter().map(|(id, value)| (id.clone(), rounded(*value))).collect();
139            frames.push(MotionFrame { time: rounded(time), poses, hidden: sample.hidden.clone(), opacity, camera, clashing });
140        }
141        Ok(Motion { fps: record.fps, duration: record.duration, frames })
142    }
143
144    /// The study's player data: the scene's solids as the document poses them
145    /// (a frame shown right now is read through its recorded originals), the
146    /// study's motion, its name as the title.
147    pub fn simulation_player_data(&mut self, study: Option<&str>) -> Result<PlayerData, String> {
148        self.simulation_bake_all(study)?;
149        self.player_data_from_playback()
150    }
151
152    /// [`Self::simulation_player_data`] over the bake as it stands.
153    fn player_data_from_playback(&self) -> Result<PlayerData, String> {
154        let motion = self.motion_from_playback()?;
155        let play = self.simulation_playback().ok_or("no study to publish")?;
156        let title = play.baked_study().name.clone();
157        let mut scene = Scene::default();
158        let flat = |values: &[[f32; 3]]| values.iter().flatten().copied().collect::<Vec<f32>>();
159        for solid in self.scene.solids() {
160            let display = play.original(&solid.name).unwrap_or(solid);
161            if display.is_sketch || display.mesh.indices.is_empty() {
162                continue;
163            }
164            let color = display.color_override.unwrap_or_else(|| crate::color::solid_color_srgb(&display.name).map(|c| c as f32));
165            scene.nodes.push(PlayerNode { occurrence: solid.name.clone(), mesh: scene.meshes.len(), matrix: IDENTITY, color });
166            scene.meshes.push(PlayerMesh::new(&solid.name, &flat(&display.mesh.positions), &flat(&display.mesh.normals), &display.mesh.indices));
167        }
168        if scene.nodes.is_empty() {
169            return Err("nothing to show: the assembly has no solids".into());
170        }
171        PlayerData::motion_study(&title, "", Target::default(), scene, motion)
172    }
173
174    /// The study's offline player: one self-contained `.html` with the data
175    /// inlined. Returns the page and the data it carries.
176    pub fn simulation_html(&mut self, study: Option<&str>) -> Result<(String, PlayerData), String> {
177        let data = self.simulation_player_data(study)?;
178        Ok((player::offline_html(&data, Mode::Customer), data))
179    }
180}