brep_render/engine_state/
simulation_ops.rs1use super::EngineState;
15use brep_simulation::model::{Simulation, BLOCK_KEY};
16use serde_json::{Map, Value};
17
18#[derive(Debug, Clone, PartialEq, serde::Serialize)]
20#[serde(rename_all = "camelCase")]
21pub struct MotorMate {
22 pub id: String,
24 pub kind: String,
26 pub value: f64,
29 pub enabled: bool,
30}
31
32impl MotorMate {
33 pub fn rate_unit(&self) -> &'static str {
35 if self.kind == "angle" { "°/s" } else { "mm/s" }
36 }
37}
38
39impl EngineState {
40 pub fn simulation(&self) -> Result<Option<Simulation>, String> {
42 match self.history.document_block(BLOCK_KEY) {
43 None | Some(Value::Null) => Ok(None),
44 Some(block) => Simulation::from_value(block).map(Some),
45 }
46 }
47
48 pub fn simulation_available(&self) -> bool {
50 self.history_has_assembly()
51 }
52
53 pub fn simulation_edit<T>(
57 &mut self,
58 coalesce: Option<&str>,
59 edit: impl FnOnce(&mut Simulation) -> Result<T, String>,
60 ) -> Result<T, String> {
61 if !self.simulation_available() {
62 return Err("open an assembly: a study drives its mates".into());
63 }
64 let mut block = self.simulation()?.unwrap_or_default();
65 let out = edit(&mut block)?;
66 block.validate()?;
67 self.history.set_document_block(BLOCK_KEY, Some(block.to_value()), coalesce);
68 Ok(out)
69 }
70
71 pub fn simulation_motor_mates(&self) -> Vec<MotorMate> {
73 let constraints = self.history.document_block("assembly").and_then(|a| a.get("constraints")).and_then(Value::as_array);
74 constraints
75 .into_iter()
76 .flatten()
77 .filter_map(|entry| {
78 let kind = entry.get("type")?.as_str()?;
79 let key = match kind {
80 "distance" => "distance",
81 "angle" => "angle",
82 _ => return None,
83 };
84 let params = entry.get("inputParams")?;
85 Some(MotorMate {
86 id: params.get("id")?.as_str()?.to_string(),
87 kind: kind.to_string(),
88 value: params.get(key).and_then(Value::as_f64).unwrap_or(0.0),
89 enabled: entry.get("enabled").and_then(Value::as_bool).unwrap_or(true),
90 })
91 })
92 .collect()
93 }
94
95 pub fn simulation_add_study(&mut self, name: &str) -> Result<String, String> {
97 self.simulation_edit(None, |block| Ok(block.add_study(name)))
98 }
99
100 pub fn simulation_set_active(&mut self, study: &str) -> Result<(), String> {
102 self.simulation_edit(None, |block| {
103 block.study(study).ok_or_else(|| format!("no study '{study}'"))?;
104 block.active = Some(study.to_string());
105 Ok(())
106 })
107 }
108
109 pub fn simulation_add_motor(&mut self, study: Option<&str>, constraint: Option<&str>) -> Result<(String, String), String> {
114 let mates = self.simulation_motor_mates();
115 let mate = match constraint {
116 Some(id) => mates.iter().find(|m| m.id == id).ok_or_else(|| format!("'{id}' is not a distance or angle mate of this assembly"))?,
117 None => mates.first().ok_or("add a distance or angle mate first: a motor drives one")?,
118 };
119 let speed = if mate.kind == "angle" { 90.0 } else { 10.0 };
120 let constraint = mate.id.clone();
121 self.simulation_edit(None, |block| {
122 let study_id = match study {
123 Some(id) => id.to_string(),
124 None => match block.active_study() {
125 Some(active) => active.id.clone(),
126 None => block.add_study(""),
127 },
128 };
129 let id = block.next_id("MOT");
130 let target = block.study_mut(&study_id).ok_or_else(|| format!("no study '{study_id}'"))?;
131 let to = target.duration;
132 target.motors.push(brep_simulation::Motor { id: id.clone(), constraint, from: 0.0, to, drive: brep_simulation::Drive::Speed { speed } });
133 Ok((study_id, id))
134 })
135 }
136
137 pub fn simulation_constraint_ids(&self) -> Vec<String> {
141 self.history
142 .document_block("assembly")
143 .and_then(|a| a.get("constraints"))
144 .and_then(Value::as_array)
145 .map(|rows| rows.iter().filter_map(|row| row.get("inputParams")?.get("id")?.as_str().map(str::to_string)).collect())
146 .unwrap_or_default()
147 }
148
149 pub fn simulation_add_clip(&mut self, study: Option<&str>, kind: &str, target: Option<&str>) -> Result<(String, String), String> {
157 use brep_simulation::ClipBody;
158 let body = match kind {
159 "disable" | "suppress" => {
162 let ids = self.simulation_constraint_ids();
163 let constraint = match target {
164 Some(id) if ids.iter().any(|c| c == id) => id.to_string(),
165 Some(id) => return Err(format!("'{id}' is not a constraint of this assembly")),
166 None => ids.first().cloned().ok_or("add a mate first: a Disable a mate clip switches one off")?,
167 };
168 ClipBody::Suppress { constraint }
169 }
170 "camera" => {
171 let views: Vec<String> = self.pmi_state().views.iter().filter(|v| v.camera.is_some()).map(|v| v.id.clone()).collect();
172 let views = match target {
173 Some(id) if views.iter().any(|v| v == id) => vec![id.to_string()],
174 Some(id) => return Err(format!("'{id}' is not a PMI view with a camera")),
175 None if views.is_empty() => return Err("capture a PMI view first: a camera clip moves between views".into()),
176 None => views,
177 };
178 ClipBody::Camera { views }
179 }
180 "visibility" => {
181 let occurrences = match target {
182 Some(id) => vec![id.to_string()],
183 None => {
184 let selected = self.selected_occurrences();
185 if selected.is_empty() {
186 vec![self.component_ids().first().cloned().ok_or("add a component first: a visibility clip hides one")?]
187 } else {
188 selected
189 }
190 }
191 };
192 ClipBody::Visibility { occurrences }
193 }
194 "explode" => {
195 let views = self.exploded_views();
196 let view = match target {
197 Some(id) if views.iter().any(|v| v.id == id) => id.to_string(),
198 Some(id) => return Err(format!("'{id}' is not an exploded view")),
199 None => views.first().map(|v| v.id.clone()).ok_or("make an exploded view first (Assembly: Auto explode): an explode clip plays its steps")?,
200 };
201 ClipBody::Explode { view, collapse: false }
202 }
203 "transparency" => {
204 let occurrences = match target {
205 Some(id) => vec![id.to_string()],
206 None => {
207 let selected = self.selected_occurrences();
208 if selected.is_empty() {
209 vec![self.component_ids().first().cloned().ok_or("add a component first: a transparency clip fades one")?]
210 } else {
211 selected
212 }
213 }
214 };
215 ClipBody::Transparency { occurrences, opacity: [1.0, 0.3] }
216 }
217 "section" => {
218 let views: Vec<String> = self.pmi_state().views.iter().filter(|v| v.display.section.is_some()).map(|v| v.id.clone()).collect();
219 let view = match target {
220 Some(id) if views.iter().any(|v| v == id) => id.to_string(),
221 Some(id) => return Err(format!("'{id}' is not a PMI view with a section")),
222 None => views.first().cloned().ok_or("give a PMI view a section first: a section clip plays it")?,
223 };
224 ClipBody::Section { view, sweep: None }
225 }
226 other => return Err(format!("no clip kind '{other}': disable (or suppress), camera, visibility, transparency, section or explode")),
227 };
228 self.simulation_edit(None, |block| {
229 let study_id = match study {
230 Some(id) => id.to_string(),
231 None => match block.active_study() {
232 Some(active) => active.id.clone(),
233 None => block.add_study(""),
234 },
235 };
236 let id = block.next_id("CLIP");
237 let target = block.study_mut(&study_id).ok_or_else(|| format!("no study '{study_id}'"))?;
238 let to = target.duration;
239 target.clips.push(brep_simulation::Clip { id: id.clone(), from: 0.0, to, body });
240 Ok((study_id, id))
241 })
242 }
243
244 pub fn simulation_remove(&mut self, study: &str, item: Option<&str>) -> Result<(), String> {
246 self.simulation_edit(None, |block| {
247 match item {
248 None => {
249 let before = block.studies.len();
250 block.studies.retain(|s| s.id != study);
251 if block.studies.len() == before {
252 return Err(format!("no study '{study}'"));
253 }
254 if block.active.as_deref() == Some(study) {
255 block.active = block.studies.first().map(|s| s.id.clone());
256 }
257 }
258 Some(id) => {
259 let target = block.study_mut(study).ok_or_else(|| format!("no study '{study}'"))?;
260 let before = target.motors.len() + target.clips.len();
261 target.motors.retain(|m| m.id != id);
262 target.clips.retain(|c| c.id != id);
263 if target.motors.len() + target.clips.len() == before {
264 return Err(format!("study '{study}' has no motor or clip '{id}'"));
265 }
266 }
267 }
268 Ok(())
269 })
270 }
271
272 pub fn simulation_state_json(&self) -> Value {
275 let block = match self.simulation() {
276 Ok(block) => block.map(|b| b.to_value()).unwrap_or(Value::Null),
277 Err(error) => serde_json::json!({ "error": error }),
278 };
279 serde_json::json!({
280 "available": self.simulation_available(),
281 "block": block,
282 "mates": self.simulation_motor_mates(),
283 })
284 }
285
286 pub fn simulation_patch(&mut self, study: &str, item: Option<&str>, patch: &Value, coalesce: Option<&str>) -> Result<(), String> {
290 let patch = patch.as_object().ok_or("a patch is an object of fields")?.clone();
291 self.simulation_edit(coalesce, |block| {
292 let target = block.study_mut(study).ok_or_else(|| format!("no study '{study}'"))?;
293 match item {
294 None => {
295 if patch.contains_key("id") || patch.contains_key("motors") || patch.contains_key("clips") {
296 return Err("a study patch edits its own fields; motors and clips are edited one by one".into());
297 }
298 *target = merged(&*target, &patch)?;
299 }
300 Some(id) => {
301 if patch.contains_key("id") {
302 return Err("an id cannot be patched".into());
303 }
304 if let Some(motor) = target.motors.iter_mut().find(|m| m.id == id) {
305 *motor = merged(&*motor, &patch)?;
306 } else if let Some(clip) = target.clips.iter_mut().find(|c| c.id == id) {
307 *clip = merged(&*clip, &patch)?;
308 } else {
309 return Err(format!("study '{study}' has no motor or clip '{id}'"));
310 }
311 }
312 }
313 Ok(())
314 })
315 }
316}
317
318fn merged<T: serde::Serialize + serde::de::DeserializeOwned>(value: &T, patch: &Map<String, Value>) -> Result<T, String> {
320 let mut object = serde_json::to_value(value).map_err(|e| e.to_string())?;
321 let fields = object.as_object_mut().ok_or("not an object")?;
322 for (key, field) in patch {
323 if field.is_null() {
324 fields.remove(key);
325 } else {
326 fields.insert(key.clone(), field.clone());
327 }
328 }
329 serde_json::from_value(object).map_err(|e| format!("that edit does not make a valid study: {e}"))
330}