use super::interference::{displays_pierce, overlaps, posed_bbox};
use super::pmi_section::{clip_display_all, side_of_all, Side};
use super::{component_move::now_ms, EngineState};
use crate::camera::Aabb;
use crate::scene::SolidDisplay;
use brep_kernel::{PmiCamera, PmiProjection};
use brep_simulation::model::Study;
use brep_simulation::sample::SectionSample;
use brep_simulation::FrameSample;
use serde_json::{json, Map, Value};
use std::collections::{BTreeMap, HashMap};
const BAKE_BUDGET_MS: f64 = 12.0;
#[derive(Debug, Clone, PartialEq)]
pub struct BakedFrame {
pub poses: BTreeMap<String, [f64; 16]>,
pub failed: bool,
pub clashes: Vec<(String, String)>,
}
pub(super) const IDENTITY: [f64; 16] = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0];
const CLASH_TINT: [f32; 3] = [0.92, 0.25, 0.2];
#[derive(Debug)]
pub struct SimulationPlayback {
pub study: String,
baked_study: Study,
revision: u64,
base: HashMap<String, f64>,
pub frames: Vec<BakedFrame>,
total: usize,
reference: Map<String, Value>,
pub time: f64,
pub playing: bool,
pub looping: bool,
shown: Option<usize>,
wanted: Option<usize>,
originals: HashMap<String, SolidDisplay>,
applied: HashMap<String, [f64; 16]>,
pub error: Option<String>,
participants: Vec<(String, Vec<String>)>,
tinted: Vec<String>,
hidden: Vec<String>,
translucent: Vec<String>,
sectioned: Vec<String>,
camera_before: Option<PmiCamera>,
shown_sample: Option<FrameSample>,
}
impl SimulationPlayback {
pub(crate) fn baked_study(&self) -> &Study {
&self.baked_study
}
pub(crate) fn revision(&self) -> u64 {
self.revision
}
pub(crate) fn base(&self) -> &HashMap<String, f64> {
&self.base
}
pub(crate) fn original(&self, name: &str) -> Option<&SolidDisplay> {
self.originals.get(name)
}
}
impl EngineState {
pub(crate) fn simulation_playback(&self) -> Option<&SimulationPlayback> {
self.simulation_play.as_ref()
}
pub fn simulation_bake_all(&mut self, study: Option<&str>) -> Result<String, String> {
let id = self.simulation_prepare(study)?;
if let Some(play) = self.simulation_play.as_mut() {
play.playing = false;
}
self.ensure_assembly_session();
let mut guard = 0usize;
while self.simulation_play.as_ref().is_some_and(|play| play.frames.len() < play.total) {
guard += 1;
if guard > 1_000_000 {
return Err("the bake did not finish".into());
}
self.simulation_bake_next();
}
Ok(id)
}
pub(super) fn simulation_keep_bake_through_publish(&mut self, study: &str, motion: Value) {
let revision = self.history.revision();
if let Some(play) = self.simulation_play.as_mut().filter(|play| play.study == study) {
play.baked_study.motion = Some(motion);
play.revision = revision;
}
}
pub fn simulation_play(&mut self, study: Option<&str>, looping: Option<bool>) -> Result<String, String> {
let id = self.simulation_prepare(study)?;
let play = self.simulation_play.as_mut().expect("prepared");
if let Some(looping) = looping {
play.looping = looping;
}
if play.time >= play.baked_study.duration - 1e-9 {
play.time = 0.0;
}
play.playing = true;
play.wanted = Some(play.baked_study.frame_at(play.time));
Ok(id)
}
pub fn simulation_pause(&mut self) {
if let Some(play) = self.simulation_play.as_mut() {
play.playing = false;
}
}
pub fn simulation_stop(&mut self) {
self.simulation_restore_displays();
if let Some(play) = self.simulation_play.as_mut() {
play.playing = false;
play.time = 0.0;
play.wanted = None;
}
}
pub fn simulation_seek(&mut self, study: Option<&str>, time: f64) -> Result<(), String> {
if !time.is_finite() {
return Err("seek: the time is not a number".into());
}
self.simulation_prepare(study)?;
let play = self.simulation_play.as_mut().expect("prepared");
play.playing = false;
play.time = time.clamp(0.0, play.baked_study.duration);
play.wanted = Some(play.baked_study.frame_at(play.time));
self.simulation_show_wanted();
Ok(())
}
pub fn simulation_set_looping(&mut self, study: Option<&str>, looping: bool) -> Result<(), String> {
self.simulation_prepare(study)?;
self.simulation_play.as_mut().expect("prepared").looping = looping;
Ok(())
}
pub fn simulation_playing_or_shown(&self) -> bool {
self.simulation_play.as_ref().is_some_and(|play| play.playing || play.shown.is_some())
}
fn simulation_prepare(&mut self, study: Option<&str>) -> Result<String, String> {
let block = self.simulation()?.ok_or("no study yet: New study starts one")?;
let study = match study {
Some(id) => block.study(id).ok_or_else(|| format!("no study '{id}'"))?,
None => block.active_study().ok_or("no study yet: New study starts one")?,
}
.clone();
let revision = self.history.revision();
let current = self
.simulation_play
.as_ref()
.is_some_and(|play| play.study == study.id && play.revision == revision && play.baked_study == study);
if current {
return Ok(study.id);
}
self.simulation_restore_displays();
if self.pmi_active_view.is_some() {
self.pmi_deactivate_view();
}
let previous = self.simulation_play.take();
let (time, looping) = previous
.filter(|play| play.study == study.id)
.map_or((0.0, false), |play| (play.time.min(study.duration), play.looping));
let base = self.simulation_motor_mates().into_iter().map(|mate| (mate.id, mate.value)).collect();
let participants = if study.interference.enabled { self.interference_participants() } else { Vec::new() };
let total = study.frame_count();
let id = study.id.clone();
self.simulation_play = Some(SimulationPlayback {
study: id.clone(),
baked_study: study,
revision,
base,
frames: Vec::with_capacity(total),
total,
reference: Map::new(),
time,
playing: false,
looping,
shown: None,
wanted: None,
originals: HashMap::new(),
applied: HashMap::new(),
error: None,
participants,
tinted: Vec::new(),
hidden: Vec::new(),
translucent: Vec::new(),
sectioned: Vec::new(),
camera_before: None,
shown_sample: None,
});
Ok(id)
}
pub fn simulation_tick(&mut self, dt: f64) -> bool {
let Some(play) = self.simulation_play.as_ref() else { return false };
if play.revision != self.history.revision() {
let (study, time, playing, looping) = (play.study.clone(), play.time, play.playing, play.looping);
self.simulation_restore_displays();
self.simulation_play = None;
if self.simulation_prepare(Some(&study)).is_err() {
return false;
}
let play = self.simulation_play.as_mut().expect("prepared");
play.time = time.min(play.baked_study.duration);
play.playing = playing;
play.looping = looping;
play.wanted = Some(play.baked_study.frame_at(play.time));
}
let start = now_ms();
if self.simulation_play.as_ref().is_some_and(|play| play.frames.len() < play.total) {
self.ensure_assembly_session();
}
while self.simulation_play.as_ref().is_some_and(|play| play.frames.len() < play.total) && now_ms() - start < BAKE_BUDGET_MS {
self.simulation_bake_next();
}
let play = self.simulation_play.as_mut().expect("checked above");
if play.playing {
let duration = play.baked_study.duration;
play.time += dt.max(0.0);
if play.time >= duration - 1e-9 {
if play.looping {
play.time = (play.time % duration).max(0.0);
} else {
play.time = duration;
play.playing = false;
}
}
let baked = play.frames.len();
if play.baked_study.frame_at(play.time) >= baked {
play.time = play.baked_study.frame_time(baked.saturating_sub(1));
if baked == 0 {
play.time = 0.0;
}
}
play.wanted = Some(play.baked_study.frame_at(play.time));
}
self.simulation_show_wanted();
let play = self.simulation_play.as_ref().expect("checked above");
play.playing || play.frames.len() < play.total
}
pub(super) fn simulation_bake_next(&mut self) {
let Some(play) = self.simulation_play.as_mut() else { return };
let index = play.frames.len();
if index >= play.total {
return;
}
let time = play.baked_study.frame_time(index);
let base = &play.base;
let sample = play.baked_study.sample(time, &|id| base.get(id).copied());
let mut overrides = Map::new();
for (id, mate) in &sample.mates {
let mut change = Map::new();
if let Some(value) = mate.value {
change.insert("value".into(), json!(value));
}
if let Some(enabled) = mate.enabled {
change.insert("enabled".into(), json!(enabled));
}
overrides.insert(id.clone(), Value::Object(change));
}
let request = json!({ "overrides": overrides, "reference": play.reference });
let (poses, failed): (BTreeMap<String, [f64; 16]>, bool) = match brep_kernel::assembly_frame_solve_impl(&request.to_string()) {
Ok(reply) => {
let reply: Value = serde_json::from_str(&reply).unwrap_or(Value::Null);
let poses_json = reply.get("poses").and_then(Value::as_object).cloned().unwrap_or_default();
let poses = poses_json.iter().map(|(id, pose)| (id.clone(), crate::pipeline::pose_params_to_transform(pose))).collect();
play.reference = poses_json;
(poses, false)
}
Err(error) => {
let poses = play.frames.last().map(|frame| frame.poses.clone()).unwrap_or_default();
play.error = Some(format!("frame {index} ({time:.3} s): {error}"));
(poses, true)
}
};
let participants = std::mem::take(&mut play.participants);
let previous = play.frames.last().cloned();
let clashes = if participants.is_empty() { Vec::new() } else { self.simulation_frame_clashes(&participants, &poses, previous.as_ref()) };
let Some(play) = self.simulation_play.as_mut() else { return };
play.participants = participants;
play.frames.push(BakedFrame { poses, failed, clashes });
}
fn simulation_frame_clashes(&self, participants: &[(String, Vec<String>)], poses: &BTreeMap<String, [f64; 16]>, previous: Option<&BakedFrame>) -> Vec<(String, String)> {
let originals = self.simulation_play.as_ref().map(|play| &play.originals);
let display_of = |name: &String| -> Option<&SolidDisplay> { originals.and_then(|originals| originals.get(name)).or_else(|| self.scene.solid(name)) };
let delta_of = |occ: &str| -> [f64; 16] {
let top = occ.split(':').next().unwrap_or(occ);
let (key, world) = match poses.get(occ) {
Some(world) => (occ, world),
None => match poses.get(top) {
Some(world) => (top, world),
None => return IDENTITY,
},
};
match self.occurrence_document_matrix(key) {
Some(document) => mat_mul(world, &rigid_inverse(&document)),
None => IDENTITY,
}
};
let mut posed: Vec<(&str, [f64; 16], Aabb, &[String])> = Vec::new();
for (occ, solids) in participants {
let delta = delta_of(occ);
let mut bbox = Aabb::empty();
for name in solids {
if let Some(solid) = display_of(name) {
bbox.union(&posed_bbox(&solid.bbox, &delta));
}
}
if !bbox.is_empty() {
posed.push((occ.as_str(), delta, bbox, solids.as_slice()));
}
}
let same_as_previous = |occ: &str, prev: &BakedFrame| {
let top = occ.split(':').next().unwrap_or(occ);
poses.get(occ) == prev.poses.get(occ) && poses.get(top) == prev.poses.get(top)
};
let mut clashes = Vec::new();
for i in 0..posed.len() {
for j in i + 1..posed.len() {
if !overlaps(&posed[i].2, &posed[j].2) {
continue;
}
let pair = (posed[i].0.to_string(), posed[j].0.to_string());
if let Some(prev) = previous {
if same_as_previous(&pair.0, prev) && same_as_previous(&pair.1, prev) {
if prev.clashes.contains(&pair) {
clashes.push(pair);
}
continue;
}
}
let hit = posed[i].3.iter().filter_map(display_of).any(|a| posed[j].3.iter().filter_map(display_of).any(|b| displays_pierce(a, &posed[i].1, b, &posed[j].1)));
if hit {
clashes.push(pair);
}
}
}
clashes
}
fn simulation_show_wanted(&mut self) {
let Some(play) = self.simulation_play.as_ref() else { return };
let Some(wanted) = play.wanted else { return };
if wanted >= play.frames.len() || play.shown == Some(wanted) {
return;
}
self.simulation_pose_frame(wanted);
}
pub(crate) fn simulation_frame_worlds(&self, index: usize) -> Option<BTreeMap<String, [f64; 16]>> {
let play = self.simulation_play.as_ref()?;
let frame = play.frames.get(index)?;
let mut worlds = frame.poses.clone();
let time = play.baked_study.frame_time(index);
let base = &play.base;
let sample = play.baked_study.sample(time, &|id| base.get(id).copied());
for explode in &sample.explode {
let Some(view) = self.exploded_views().into_iter().find(|view| view.id == explode.view) else { continue };
let steps: Vec<_> = view.steps.iter().filter(|step| step.enabled).collect();
let count = steps.len() as f64;
for (k, step) in steps.iter().enumerate() {
let fraction = (explode.progress * count - k as f64).clamp(0.0, 1.0);
if fraction <= 0.0 {
continue;
}
for target in &step.targets {
let Some(world) = worlds.get(target).copied().or_else(|| self.occurrence_document_matrix(target)) else { continue };
let mut shift = IDENTITY;
for axis in 0..3 {
shift[axis * 4 + 3] = step.translate[axis] * fraction;
}
worlds.insert(target.clone(), mat_mul(&shift, &world));
}
}
}
Some(worlds)
}
fn simulation_pose_frame(&mut self, index: usize) {
let Some(play) = self.simulation_play.as_ref() else { return };
if index >= play.frames.len() {
return;
}
let time = play.baked_study.frame_time(index);
let base = play.base.clone();
let sample = play.baked_study.sample(time, &|id| base.get(id).copied());
let Some(worlds) = self.simulation_frame_worlds(index) else { return };
let Some(play) = self.simulation_play.as_ref() else { return };
let frame = worlds;
let gone: Vec<String> = play.applied.keys().filter(|id| !frame.contains_key(*id)).cloned().collect();
self.simulation_unsection();
let Some(play) = self.simulation_play.as_ref() else { return };
let mut changed = false;
for id in gone {
self.simulation_pose_occurrence(&id, None);
changed = true;
}
for (id, world) in &frame {
let Some(document) = self.occurrence_document_matrix(id) else { continue };
let shown = self.simulation_play.as_ref().and_then(|play| play.applied.get(id).copied()).unwrap_or(document);
if same_matrix(&shown, world) {
continue;
}
if same_matrix(world, &document) {
self.simulation_pose_occurrence(id, None);
} else {
let delta = mat_mul(world, &rigid_inverse(&document));
self.simulation_pose_occurrence(id, Some((delta, *world)));
}
changed = true;
}
if let Some(play) = self.simulation_play.as_mut() {
play.shown = Some(index);
}
if changed {
self.dirty = true;
}
self.simulation_apply_section(sample.section.as_ref());
self.simulation_tint_clashes(index);
self.simulation_apply_hidden(&sample.hidden);
self.simulation_apply_opacity(&sample.opacity);
if let Some(camera) = &sample.camera {
self.simulation_apply_camera(&camera.from, &camera.to, camera.t);
}
if let Some(play) = self.simulation_play.as_mut() {
play.shown_sample = Some(sample);
}
}
fn simulation_unsection(&mut self) {
let Some(play) = self.simulation_play.as_mut() else { return };
let sectioned = std::mem::take(&mut play.sectioned);
if sectioned.is_empty() {
return;
}
let applied: Vec<(String, [f64; 16])> = play.applied.iter().map(|(id, world)| (id.clone(), *world)).collect();
let mut reposed: Vec<String> = Vec::new();
for name in §ioned {
if let (Some(original), Some(display)) = (play.originals.get(name), self.scene.solid_mut(name)) {
*display = original.clone();
}
if let Some((id, _)) = applied.iter().filter(|(id, _)| name.starts_with(&format!("{id}:"))).max_by_key(|(id, _)| id.len()) {
if !reposed.contains(id) {
reposed.push(id.clone());
}
}
}
for id in reposed {
let Some(world) = applied.iter().find(|(key, _)| *key == id).map(|(_, world)| *world) else { continue };
let Some(document) = self.occurrence_document_matrix(&id) else { continue };
self.simulation_pose_occurrence(&id, Some((mat_mul(&world, &rigid_inverse(&document)), world)));
}
self.dirty = true;
}
fn simulation_apply_section(&mut self, section: Option<&SectionSample>) {
let Some(section) = section else { return };
let state = self.pmi_state_shared();
let Some(view) = state.find_view(§ion.view) else { return };
let report = self.pmi_report.as_ref().and_then(|report| report.view(§ion.view));
let planes: Vec<([f64; 3], [f64; 3])> = view
.section_planes_in(report)
.into_iter()
.map(|(point, normal)| (std::array::from_fn(|i| point[i] + normal[i] * section.offset), normal))
.collect();
if planes.is_empty() {
return;
}
let exclude = view.display.section.clone();
let names: Vec<(String, Side)> = self
.scene
.solids()
.iter()
.filter(|solid| solid.visible)
.map(|solid| (solid.name.clone(), if exclude.as_ref().is_some_and(|cut| cut.excludes(&solid.name)) { Side::Kept } else { side_of_all(solid, &planes) }))
.collect();
let mut sectioned = Vec::new();
for (name, side) in names {
if side == Side::Kept {
continue;
}
let Some(display) = self.scene.solid(&name).cloned() else { continue };
if let Some(play) = self.simulation_play.as_mut() {
play.originals.entry(name.clone()).or_insert(display);
}
if let Some(display) = self.scene.solid_mut(&name) {
match side {
Side::Removed => display.visible = false,
Side::Cut => clip_display_all(display, &planes),
Side::Kept => {}
}
display.revision = crate::scene::next_revision();
}
sectioned.push(name);
}
if let Some(play) = self.simulation_play.as_mut() {
play.sectioned = sectioned;
}
self.dirty = true;
}
fn simulation_apply_opacity(&mut self, opacity: &BTreeMap<String, f64>) {
let mut want: Vec<(String, f32)> = Vec::new();
for (id, value) in opacity {
for name in self.simulation_occurrence_solids(id) {
want.push((name, value.clamp(0.0, 1.0) as f32));
}
}
want.sort_by(|a, b| a.0.cmp(&b.0));
want.dedup_by(|a, b| a.0 == b.0);
let Some(play) = self.simulation_play.as_ref() else { return };
let previous = play.translucent.clone();
let mut changed = false;
for name in previous.iter().filter(|name| !want.iter().any(|(n, _)| n == *name)) {
let opacity = self.simulation_play.as_ref().and_then(|play| play.originals.get(name)).map(|original| original.opacity);
if let (Some(opacity), Some(display)) = (opacity, self.scene.solid_mut(name)) {
if display.opacity != opacity {
display.opacity = opacity;
display.revision = crate::scene::next_revision();
changed = true;
}
}
}
for (name, value) in &want {
let Some(display) = self.scene.solid(name).cloned() else { continue };
if let Some(play) = self.simulation_play.as_mut() {
play.originals.entry(name.clone()).or_insert(display);
}
if let Some(display) = self.scene.solid_mut(name) {
if display.opacity != Some(*value) {
display.opacity = Some(*value);
display.revision = crate::scene::next_revision();
changed = true;
}
}
}
if let Some(play) = self.simulation_play.as_mut() {
play.translucent = want.into_iter().map(|(name, _)| name).collect();
}
if changed {
self.dirty = true;
}
}
fn simulation_occurrence_solids(&self, id: &str) -> Vec<String> {
if id.contains(':') {
let prefix = format!("{id}:");
self.scene.solids().iter().filter(|solid| solid.name.starts_with(&prefix)).map(|solid| solid.name.clone()).collect()
} else {
self.member_solid_names(id)
}
}
fn simulation_apply_hidden(&mut self, occurrences: &[String]) {
let mut want: Vec<String> = occurrences.iter().flat_map(|id| self.simulation_occurrence_solids(id)).collect();
want.sort();
want.dedup();
let Some(play) = self.simulation_play.as_ref() else { return };
let previous = play.hidden.clone();
let mut changed = false;
for name in previous.iter().filter(|name| !want.contains(name)) {
let visible = self.simulation_play.as_ref().and_then(|play| play.originals.get(name)).map(|original| original.visible);
if let (Some(visible), Some(display)) = (visible, self.scene.solid_mut(name)) {
if display.visible != visible {
display.visible = visible;
changed = true;
}
}
}
for name in &want {
let Some(display) = self.scene.solid(name).cloned() else { continue };
if let Some(play) = self.simulation_play.as_mut() {
play.originals.entry(name.clone()).or_insert(display);
}
if let Some(display) = self.scene.solid_mut(name) {
if display.visible {
display.visible = false;
changed = true;
}
}
}
if let Some(play) = self.simulation_play.as_mut() {
play.hidden = want;
}
if changed {
self.dirty = true;
}
}
fn simulation_apply_camera(&mut self, from: &str, to: &str, t: f64) {
let Some(camera) = self.simulation_camera_between(from, to, t) else { return };
let standing = self.snapshot_camera();
if let Some(play) = self.simulation_play.as_mut() {
if play.camera_before.is_none() {
play.camera_before = Some(standing);
}
}
self.simulation_apply_pmi_camera(&camera);
}
pub(crate) fn simulation_camera_between(&self, from: &str, to: &str, t: f64) -> Option<PmiCamera> {
let state = self.pmi_state_shared();
let a = state.find_view(from).and_then(|v| v.camera.clone())?;
let b = state.find_view(to).and_then(|v| v.camera.clone())?;
let t = t.clamp(0.0, 1.0);
let lerp = |p: [f64; 3], q: [f64; 3]| -> [f64; 3] { std::array::from_fn(|i| p[i] + (q[i] - p[i]) * t) };
let scale_of = |camera: &PmiCamera| match camera.projection {
PmiProjection::Orthographic { half_height } => half_height,
PmiProjection::Perspective { fov_y_deg } => fov_y_deg,
};
let orthographic = |camera: &PmiCamera| matches!(camera.projection, PmiProjection::Orthographic { .. });
let same_kind = orthographic(&a) == orthographic(&b);
let (ortho, scale) = if same_kind {
(orthographic(&a), scale_of(&a) + (scale_of(&b) - scale_of(&a)) * t)
} else if t < 0.5 {
(orthographic(&a), scale_of(&a))
} else {
(orthographic(&b), scale_of(&b))
};
Some(PmiCamera {
eye: lerp(a.eye, b.eye),
target: lerp(a.target, b.target),
up: lerp(a.up, b.up),
projection: if ortho { PmiProjection::Orthographic { half_height: scale } } else { PmiProjection::Perspective { fov_y_deg: scale } },
viewport: a.viewport,
})
}
fn simulation_apply_pmi_camera(&mut self, camera: &PmiCamera) {
let (kind, scale) = match camera.projection {
PmiProjection::Orthographic { half_height } => ("orthographic", half_height),
PmiProjection::Perspective { fov_y_deg } => ("perspective", fov_y_deg),
};
let json = json!({ "kind": kind, "eye": camera.eye, "target": camera.target, "up": camera.up, "scale": scale }).to_string();
if self.apply_camera_state_json(&json).is_ok() {
self.controls_sync_after_camera_apply();
}
}
fn simulation_tint_clashes(&mut self, index: usize) {
let Some(play) = self.simulation_play.as_ref() else { return };
let Some(frame) = play.frames.get(index) else { return };
let mut want: Vec<String> = Vec::new();
for (a, b) in &frame.clashes {
for occ in [a, b] {
if let Some((_, solids)) = play.participants.iter().find(|(id, _)| id == occ) {
want.extend(solids.iter().cloned());
}
}
}
want.sort();
want.dedup();
let previous = play.tinted.clone();
let mut changed = false;
for name in previous.iter().filter(|name| !want.contains(name)) {
let color = self.simulation_play.as_ref().and_then(|play| play.originals.get(name)).map(|original| original.color_override);
if let (Some(color), Some(display)) = (color, self.scene.solid_mut(name)) {
if display.color_override != color {
display.color_override = color;
display.revision = crate::scene::next_revision();
changed = true;
}
}
}
for name in &want {
let Some(display) = self.scene.solid(name).cloned() else { continue };
if let Some(play) = self.simulation_play.as_mut() {
play.originals.entry(name.clone()).or_insert(display);
}
if let Some(display) = self.scene.solid_mut(name) {
if display.color_override != Some(CLASH_TINT) {
display.color_override = Some(CLASH_TINT);
display.revision = crate::scene::next_revision();
changed = true;
}
}
}
if let Some(play) = self.simulation_play.as_mut() {
play.tinted = want;
}
if changed {
self.dirty = true;
}
}
fn simulation_pose_occurrence(&mut self, id: &str, pose: Option<([f64; 16], [f64; 16])>) {
let names: Vec<String> = if id.contains(':') {
let prefix = format!("{id}:");
self.scene.solids().iter().filter(|solid| solid.name.starts_with(&prefix)).map(|solid| solid.name.clone()).collect()
} else {
self.member_solid_names(id)
};
let Some(play) = self.simulation_play.as_mut() else { return };
for name in names {
let Some(display) = self.scene.solid(&name) else { continue };
let original = play.originals.entry(name.clone()).or_insert_with(|| display.clone()).clone();
if let Some(display) = self.scene.solid_mut(&name) {
*display = original;
if let Some((delta, _)) = &pose {
pose_display_by_matrix(display, delta);
}
}
}
let nested = format!("{id}:");
play.applied.retain(|key, _| !key.starts_with(&nested));
match pose {
Some((_, world)) => {
play.applied.insert(id.to_string(), world);
}
None => {
play.applied.remove(id);
}
}
}
pub(super) fn occurrence_document_matrix(&self, id: &str) -> Option<[f64; 16]> {
let (top, rest) = match id.split_once(':') {
Some((top, rest)) => (top, Some(rest)),
None => (id, None),
};
let record = self.assembly_components().iter().find(|record| record.id == top)?;
match rest {
None => Some(record.transform),
Some(rest) => Some(mat_mul(&record.transform, record.nested_poses.get(rest)?)),
}
}
pub(crate) fn simulation_restore_displays(&mut self) {
let Some(play) = self.simulation_play.as_mut() else { return };
play.applied.clear();
play.tinted.clear();
play.hidden.clear();
play.translucent.clear();
play.sectioned.clear();
play.shown_sample = None;
let camera_before = play.camera_before.take();
let originals = std::mem::take(&mut play.originals);
if let Some(camera) = camera_before {
self.simulation_apply_pmi_camera(&camera);
}
let Some(play) = self.simulation_play.as_mut() else { return };
if let Some(shown) = play.shown.take() {
play.wanted.get_or_insert(shown);
}
if originals.is_empty() {
return;
}
for (name, original) in originals {
if let Some(display) = self.scene.solid_mut(&name) {
*display = original;
}
}
self.dirty = true;
}
pub(crate) fn simulation_after_apply(&mut self) {
let Some(play) = self.simulation_play.as_ref() else { return };
if play.revision == self.history.revision() {
self.simulation_show_wanted();
}
}
pub fn simulation_forget_playback(&mut self) {
self.simulation_restore_displays();
self.simulation_play = None;
}
pub fn simulation_playback_json(&self) -> Value {
let Some(play) = self.simulation_play.as_ref() else { return Value::Null };
let failed: Vec<usize> = play.frames.iter().enumerate().filter(|(_, frame)| frame.failed).map(|(index, _)| index).collect();
let poses: BTreeMap<&str, Value> = play
.applied
.iter()
.map(|(id, world)| (id.as_str(), brep_kernel::transform_to_pose_params(&brep_kernel::AffineTransform { elements: *world })))
.collect();
json!({
"study": play.study,
"time": play.time,
"playheadFrame": play.baked_study.frame_at(play.time),
"shown": play.shown,
"baked": play.frames.len(),
"total": play.total,
"playing": play.playing,
"looping": play.looping,
"failed": failed,
"error": play.error,
"poses": poses,
"interference": play.baked_study.interference.enabled,
"clashes": play.frames.iter().enumerate().filter(|(_, frame)| !frame.clashes.is_empty()).map(|(index, _)| index).collect::<Vec<usize>>(),
"shownClashes": play.shown.and_then(|index| play.frames.get(index)).map(|frame| frame.clashes.clone()).unwrap_or_default(),
"hidden": play.shown_sample.as_ref().map(|sample| sample.hidden.clone()).unwrap_or_default(),
"explode": play.shown_sample.as_ref().map(|sample| sample.explode.iter().map(|e| json!({ "view": e.view, "progress": e.progress })).collect::<Vec<Value>>()).unwrap_or_default(),
"camera": play.shown_sample.as_ref().and_then(|sample| sample.camera.as_ref()).map(|c| json!({ "from": c.from, "to": c.to, "t": c.t })),
"cameraHeld": play.camera_before.is_some(),
"opacity": play.shown_sample.as_ref().map(|sample| sample.opacity.clone()).unwrap_or_default(),
"section": play.shown_sample.as_ref().and_then(|sample| sample.section.as_ref()).map(|s| json!({ "view": s.view, "offset": s.offset })),
})
}
}
pub(super) fn mat_mul(a: &[f64; 16], b: &[f64; 16]) -> [f64; 16] {
std::array::from_fn(|k| {
let (row, col) = (k / 4, k % 4);
(0..4).map(|n| a[row * 4 + n] * b[n * 4 + col]).sum()
})
}
pub(super) fn rigid_inverse(m: &[f64; 16]) -> [f64; 16] {
let mut out = [0.0; 16];
for row in 0..3 {
for col in 0..3 {
out[row * 4 + col] = m[col * 4 + row];
}
out[row * 4 + 3] = -(0..3).map(|k| m[k * 4 + row] * m[k * 4 + 3]).sum::<f64>();
}
out[15] = 1.0;
out
}
pub(super) fn same_matrix(a: &[f64; 16], b: &[f64; 16]) -> bool {
a.iter().zip(b).all(|(x, y)| (x - y).abs() <= 1e-9)
}
fn mat4_mul_column_major(a: &[f32; 16], b: &[f32; 16]) -> [f32; 16] {
std::array::from_fn(|k| {
let (col, row) = (k / 4, k % 4);
(0..4).map(|n| a[n * 4 + row] * b[col * 4 + n]).sum()
})
}
fn pose_display_by_matrix(display: &mut SolidDisplay, m: &[f64; 16]) {
let column_major: [f32; 16] = std::array::from_fn(|k| {
let (col, row) = (k / 4, k % 4);
m[row * 4 + col] as f32
});
display.pose = if display.shape_revision == 0 {
Some(column_major)
} else {
display.pose.map(|before| mat4_mul_column_major(&column_major, &before))
};
if display.shape_revision == 0 {
display.shape_revision = display.revision;
}
display.revision = crate::scene::next_revision();
let point = |p: [f64; 3]| -> [f64; 3] {
std::array::from_fn(|i| m[i * 4] * p[0] + m[i * 4 + 1] * p[1] + m[i * 4 + 2] * p[2] + m[i * 4 + 3])
};
let vector = |v: [f64; 3]| -> [f64; 3] { std::array::from_fn(|i| m[i * 4] * v[0] + m[i * 4 + 1] * v[1] + m[i * 4 + 2] * v[2]) };
let mut bbox = crate::camera::Aabb::empty();
for position in &mut display.mesh.positions {
let posed = point(position.map(f64::from));
*position = posed.map(|v| v as f32);
bbox.expand(posed);
}
for normal in &mut display.mesh.normals {
let rotated = vector(normal.map(f64::from));
let length = rotated.iter().map(|v| v * v).sum::<f64>().sqrt().max(1e-20);
*normal = rotated.map(|v| (v / length) as f32);
}
for edge in &mut display.edges {
for p in &mut edge.polyline {
let posed = point(p.map(f64::from));
*p = posed.map(|v| v as f32);
bbox.expand(posed);
}
}
for vertex in &mut display.vertices {
vertex.position = point(vertex.position);
bbox.expand(vertex.position);
}
if !bbox.is_empty() {
display.bbox = bbox;
}
}