brep_render/engine_state/
simulation_player.rs1use 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
19pub 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
35fn rounded(value: f64) -> f64 {
37 (value * 1e4).round() / 1e4
38}
39
40fn 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 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 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 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 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 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 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 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 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}