use super::simulation_play::{mat_mul, rigid_inverse, same_matrix, IDENTITY};
use super::EngineState;
use brep_instructions::model::Target;
use brep_instructions::player::{self, Camera, Mode, Motion, MotionFrame, PlayerData, PlayerMesh, PlayerNode, Scene};
use brep_kernel::{PmiCamera, PmiProjection};
use brep_simulation::model::BLOCK_KEY;
use serde_json::{json, Value};
use std::collections::BTreeMap;
pub fn assembly_signature(document: &Value) -> String {
let mut stripped = document.clone();
if let Some(studies) = stripped.get_mut(BLOCK_KEY).and_then(|block| block.get_mut("studies")).and_then(Value::as_array_mut) {
for study in studies {
if let Some(object) = study.as_object_mut() {
object.remove("motion");
}
}
}
super::assembly_ops::document_signature(&stripped.to_string())
}
fn rounded(value: f64) -> f64 {
(value * 1e4).round() / 1e4
}
fn column_major(m: &[f64; 16]) -> [f64; 16] {
std::array::from_fn(|k| {
let (col, row) = (k / 4, k % 4);
rounded(m[row * 4 + col])
})
}
fn player_camera(camera: &PmiCamera) -> Camera {
let (fov_deg, half_height) = match camera.projection {
PmiProjection::Perspective { fov_y_deg } => (Some(fov_y_deg), None),
PmiProjection::Orthographic { half_height } => (None, Some(half_height)),
};
Camera { eye: camera.eye, target: camera.target, up: camera.up, fov_deg, half_height }
}
impl EngineState {
pub fn simulation_motion(&mut self, study: Option<&str>) -> Result<Motion, String> {
self.simulation_bake_all(study)?;
self.motion_from_playback()
}
pub fn simulation_publish(&mut self, study: Option<&str>) -> Result<(String, usize), String> {
let id = self.simulation_bake_all(study)?;
let motion = self.motion_from_playback()?;
let frames = motion.frames.len();
let document: Value = serde_json::from_str(&self.history.request_json()).map_err(|e| format!("the document does not read back: {e}"))?;
let signature = assembly_signature(&document);
let mut value = serde_json::to_value(&motion).expect("a motion serializes");
value["study"] = json!(id);
value["assemblySignature"] = json!(signature);
let written = value.clone();
self.simulation_edit(None, |block| {
let study = block.study_mut(&id).ok_or_else(|| format!("no study '{id}'"))?;
study.motion = Some(value);
Ok(())
})?;
self.simulation_keep_bake_through_publish(&id, written);
Ok((id, frames))
}
pub fn simulation_player_data_baked(&self) -> Result<PlayerData, String> {
let play = self.simulation_playback().ok_or("no study has been played or scrubbed yet")?;
let total = play.baked_study().frame_count();
if play.frames.len() < total {
return Err(format!("the study's bake is not finished ({} of {total} frames): let it play once", play.frames.len()));
}
if play.revision() != self.history.revision() {
return Err("the study or the assembly changed since its bake: play it again".into());
}
self.player_data_from_playback()
}
fn motion_from_playback(&self) -> Result<Motion, String> {
let play = self.simulation_playback().ok_or("no study to publish")?;
let record = play.baked_study().clone();
let base = play.base().clone();
let count = play.frames.len();
let mut frames = Vec::with_capacity(count);
for index in 0..count {
let time = record.frame_time(index);
let sample = record.sample(time, &|id| base.get(id).copied());
let worlds = self.simulation_frame_worlds(index).unwrap_or_default();
let mut poses = BTreeMap::new();
for (occurrence, world) in &worlds {
let Some(document) = self.occurrence_document_matrix(occurrence) else { continue };
let delta = mat_mul(world, &rigid_inverse(&document));
if !same_matrix(&delta, &IDENTITY) {
poses.insert(occurrence.clone(), column_major(&delta));
}
}
let camera = sample.camera.as_ref().and_then(|c| self.simulation_camera_between(&c.from, &c.to, c.t)).map(|c| player_camera(&c));
let mut clashing: Vec<String> = Vec::new();
if let Some(frame) = self.simulation_playback().and_then(|play| play.frames.get(index)) {
for (a, b) in &frame.clashes {
for id in [a, b] {
if !clashing.contains(id) {
clashing.push(id.clone());
}
}
}
}
let opacity = sample.opacity.iter().map(|(id, value)| (id.clone(), rounded(*value))).collect();
frames.push(MotionFrame { time: rounded(time), poses, hidden: sample.hidden.clone(), opacity, camera, clashing });
}
Ok(Motion { fps: record.fps, duration: record.duration, frames })
}
pub fn simulation_player_data(&mut self, study: Option<&str>) -> Result<PlayerData, String> {
self.simulation_bake_all(study)?;
self.player_data_from_playback()
}
fn player_data_from_playback(&self) -> Result<PlayerData, String> {
let motion = self.motion_from_playback()?;
let play = self.simulation_playback().ok_or("no study to publish")?;
let title = play.baked_study().name.clone();
let mut scene = Scene::default();
let flat = |values: &[[f32; 3]]| values.iter().flatten().copied().collect::<Vec<f32>>();
for solid in self.scene.solids() {
let display = play.original(&solid.name).unwrap_or(solid);
if display.is_sketch || display.mesh.indices.is_empty() {
continue;
}
let color = display.color_override.unwrap_or_else(|| crate::color::solid_color_srgb(&display.name).map(|c| c as f32));
scene.nodes.push(PlayerNode { occurrence: solid.name.clone(), mesh: scene.meshes.len(), matrix: IDENTITY, color });
scene.meshes.push(PlayerMesh::new(&solid.name, &flat(&display.mesh.positions), &flat(&display.mesh.normals), &display.mesh.indices));
}
if scene.nodes.is_empty() {
return Err("nothing to show: the assembly has no solids".into());
}
PlayerData::motion_study(&title, "", Target::default(), scene, motion)
}
pub fn simulation_html(&mut self, study: Option<&str>) -> Result<(String, PlayerData), String> {
let data = self.simulation_player_data(study)?;
Ok((player::offline_html(&data, Mode::Customer), data))
}
}