Skip to main content

brep_simulation/
sample.rs

1//! A study at one time: what the solver and the display are asked for.
2//!
3//! [`Study::sample`] reads the motors and clips at a time and returns a
4//! [`FrameSample`]: per mate, the value it is driven to or that it is off; and
5//! the camera, hidden and transparent occurrences, section and explode state.
6//! Motors hold their last value after their span and leave the mate at its
7//! document value before it. Visibility, transparency and section clips act
8//! only inside their span; camera and explode clips hold their last state
9//! after it.
10
11use crate::model::{ClipBody, Drive, Motor, Shape, Study, DEFAULT_ACCEL};
12use std::collections::BTreeMap;
13
14/// What one mate does at a frame.
15#[derive(Debug, Clone, Copy, PartialEq, Default)]
16pub struct MateOverride {
17    /// The value the mate is driven to (model units or degrees).
18    pub value: Option<f64>,
19    /// `false` when a suppress clip has it off.
20    pub enabled: Option<bool>,
21}
22
23/// The camera between two PMI views: `from` blending into `to` by `t` (0..1).
24#[derive(Debug, Clone, PartialEq)]
25pub struct CameraSample {
26    pub from: String,
27    pub to: String,
28    pub t: f64,
29}
30
31#[derive(Debug, Clone, PartialEq)]
32pub struct SectionSample {
33    pub view: String,
34    /// How far the view's planes move along their normals.
35    pub offset: f64,
36}
37
38#[derive(Debug, Clone, PartialEq)]
39pub struct ExplodeSample {
40    pub view: String,
41    /// 0 collapsed .. 1 every step out.
42    pub progress: f64,
43}
44
45#[derive(Debug, Clone, PartialEq, Default)]
46pub struct FrameSample {
47    /// Constraint id → its override; mates absent here keep their document state.
48    pub mates: BTreeMap<String, MateOverride>,
49    pub camera: Option<CameraSample>,
50    /// Hidden occurrences.
51    pub hidden: Vec<String>,
52    /// Occurrence → opacity (0..1) for the transparent ones.
53    pub opacity: BTreeMap<String, f64>,
54    pub section: Option<SectionSample>,
55    /// The exploded views in play, in clip order (later ones compose on top).
56    pub explode: Vec<ExplodeSample>,
57}
58
59/// Position along a profile, 0..1, at the span fraction `u` (0..1).
60pub fn shape_at(shape: Shape, accel: Option<f64>, u: f64) -> f64 {
61    let u = u.clamp(0.0, 1.0);
62    match shape {
63        Shape::Linear => u,
64        Shape::Ease => u * u * (3.0 - 2.0 * u),
65        Shape::Trapezoid => {
66            let a = accel.unwrap_or(DEFAULT_ACCEL).clamp(1e-6, 0.5);
67            // Peak rate so the area under the trapezoid is 1.
68            let peak = 1.0 / (1.0 - a);
69            if u < a {
70                0.5 * peak * u * u / a
71            } else if u <= 1.0 - a {
72                peak * (u - 0.5 * a)
73            } else {
74                1.0 - 0.5 * peak * (1.0 - u) * (1.0 - u) / a
75            }
76        }
77    }
78}
79
80impl Motor {
81    /// The motor's value at `time`, given the mate's document value `base`
82    /// (what a speed drive starts from); `None` before its span.
83    pub fn value_at(&self, time: f64, base: f64) -> Option<f64> {
84        if time < self.from {
85            return None;
86        }
87        let elapsed = time.min(self.to) - self.from;
88        Some(match &self.drive {
89            Drive::Speed { speed } => base + speed * elapsed,
90            Drive::Profile { shape, start, end, accel } => start + (end - start) * shape_at(*shape, *accel, elapsed / (self.to - self.from)),
91        })
92    }
93}
94
95impl Study {
96    /// The study at `time`. `base` gives a mate's document value (by
97    /// constraint id), which a speed motor starts from; a mate it does not
98    /// know starts at 0.
99    pub fn sample(&self, time: f64, base: &dyn Fn(&str) -> Option<f64>) -> FrameSample {
100        let mut out = FrameSample::default();
101        for motor in &self.motors {
102            // Motors on one mate never overlap (validated): the latest started wins.
103            if let Some(value) = motor.value_at(time, base(&motor.constraint).unwrap_or(0.0)) {
104                let entry = out.mates.entry(motor.constraint.clone()).or_default();
105                let later = self
106                    .motors
107                    .iter()
108                    .any(|other| other.constraint == motor.constraint && other.from > motor.from && other.from <= time);
109                if !later {
110                    entry.value = Some(value);
111                }
112            }
113        }
114        // In start order, so a later clip's camera and explode state win.
115        let mut clips: Vec<_> = self.clips.iter().collect();
116        clips.sort_by(|a, b| a.from.total_cmp(&b.from));
117        let mut camera: Option<CameraSample> = None;
118        for clip in clips {
119            let inside = time >= clip.from && time < clip.to;
120            let u = ((time - clip.from) / (clip.to - clip.from)).clamp(0.0, 1.0);
121            match &clip.body {
122                ClipBody::Suppress { constraint } if inside => {
123                    out.mates.entry(constraint.clone()).or_default().enabled = Some(false);
124                }
125                ClipBody::Camera { views } if time >= clip.from => {
126                    camera = Some(if views.len() == 1 {
127                        CameraSample { from: views[0].clone(), to: views[0].clone(), t: 0.0 }
128                    } else {
129                        let along = u * (views.len() - 1) as f64;
130                        let at = (along.floor() as usize).min(views.len() - 2);
131                        CameraSample { from: views[at].clone(), to: views[at + 1].clone(), t: along - at as f64 }
132                    });
133                }
134                ClipBody::Visibility { occurrences } if inside => {
135                    for occurrence in occurrences {
136                        if !out.hidden.contains(occurrence) {
137                            out.hidden.push(occurrence.clone());
138                        }
139                    }
140                }
141                ClipBody::Transparency { occurrences, opacity } if inside => {
142                    let value = opacity[0] + (opacity[1] - opacity[0]) * u;
143                    for occurrence in occurrences {
144                        out.opacity.insert(occurrence.clone(), value);
145                    }
146                }
147                ClipBody::Section { view, sweep } if inside => {
148                    let offset = sweep.map_or(0.0, |s| s[0] + (s[1] - s[0]) * u);
149                    out.section = Some(SectionSample { view: view.clone(), offset });
150                }
151                ClipBody::Explode { view, collapse } if time >= clip.from => {
152                    let progress = if *collapse { 1.0 - u } else { u };
153                    out.explode.retain(|e| e.view != *view);
154                    if progress > 0.0 {
155                        out.explode.push(ExplodeSample { view: view.clone(), progress });
156                    }
157                }
158                _ => {}
159            }
160        }
161        out.camera = camera;
162        out
163    }
164}
165