1use super::assembly_ops::ComponentInsert;
29use super::EngineState;
30use super::{InstructionsRefField, RefSelectTarget};
31use brep_instructions::model::{Arrow, Instructions, Step, Style, Target, BLOCK_KEY, DOCUMENT_CLASS};
32use brep_instructions::wizard::Level;
33use brep_instructions::{edit, retarget, source, wizard};
34use brep_kernel::{PmiAnnotation, PmiCamera, PmiDisplay, PmiProjection, PmiView};
35use serde_json::{json, Value};
36
37const GHOST_RGBA: [f32; 4] = [0.62, 0.64, 0.66, 0.28];
39
40const INSERTION_SECONDS: f64 = 0.8;
42const GENERATED_VIEWPORT: [f64; 2] = [1280.0, 800.0];
45
46#[derive(Debug, Clone, PartialEq)]
50pub struct Insertion {
51 view: String,
53 moves: Vec<(Vec<String>, [f64; 3])>,
55 fraction: f64,
57 held: bool,
59}
60
61#[derive(Debug, Clone, PartialEq)]
63pub enum PlayTo {
64 First,
65 Next,
66 Back,
67 Replay,
69 Step(String),
70}
71
72const DOCUMENT_CLASS_KEY: &str = "documentClass";
74
75impl EngineState {
76 pub fn instructions(&self) -> Result<Option<Instructions>, String> {
78 match self.history.document_block(BLOCK_KEY) {
79 None | Some(Value::Null) => Ok(None),
80 Some(block) => Instructions::from_value(block).map(Some),
81 }
82 }
83
84 pub fn is_instructions(&self) -> bool {
86 self.history.document_block(DOCUMENT_CLASS_KEY).and_then(Value::as_str) == Some(DOCUMENT_CLASS)
87 || self.history.document_block(BLOCK_KEY).is_some()
88 }
89
90 pub(super) fn instructions_required(&self) -> Result<Instructions, String> {
91 self.instructions()?.ok_or_else(|| "this document is not manufacturing instructions".to_string())
92 }
93
94 pub fn instructions_edit<T>(
97 &mut self,
98 coalesce: Option<&str>,
99 edit: impl FnOnce(&mut Instructions) -> Result<T, String>,
100 ) -> Result<T, String> {
101 let mut block = self.instructions_required()?;
102 let out = edit(&mut block)?;
103 block.validate()?;
104 self.history.set_document_block(BLOCK_KEY, Some(block.to_value()), coalesce);
105 Ok(out)
106 }
107
108 pub fn instructions_wrapper(&self) -> Result<(String, String), String> {
110 self.history
111 .features()
112 .iter()
113 .find(|f| super::components::is_acomp_feature_type(f["type"].as_str().unwrap_or_default()))
114 .and_then(|f| Some((f["inputParams"]["id"].as_str()?.to_string(), f["inputParams"]["partName"].as_str()?.to_string())))
115 .ok_or_else(|| "the instructions place no assembly".to_string())
116 }
117
118 pub fn instructions_level(&self) -> Result<Level, String> {
120 let (id, part) = self.instructions_wrapper()?;
121 let document = &self.history.parts_library()[part.as_str()]["document"];
122 if !source::has_placements(document) {
123 return Err(format!("'{part}' is not an assembly: instructions need one to describe"));
124 }
125 Ok(source::level_from_document(document, &id, "", &part))
126 }
127
128 pub fn instructions_start(&mut self, insert: ComponentInsert<'_>, target: Target) -> Result<String, String> {
132 if !self.history.features().is_empty() || self.history.document_block(BLOCK_KEY).is_some() {
133 return Err("instructions start from an empty document".into());
134 }
135 if let ComponentInsert::New { document_json, .. } = &insert {
136 let document: Value = serde_json::from_str(document_json).map_err(|e| format!("the assembly is not a document: {e}"))?;
137 if !source::has_placements(&document) {
138 return Err("instructions need an assembly to describe".into());
139 }
140 }
141 let id = self.insert_component(insert)?;
143 self.history.set_document_block_no_undo(DOCUMENT_CLASS_KEY, Some(Value::String(DOCUMENT_CLASS.into())));
144 self.history.set_document_block_no_undo(BLOCK_KEY, Some(Instructions::new(target).to_value()));
145 Ok(id)
146 }
147
148 pub fn instructions_run_wizard(&mut self) -> Result<usize, String> {
153 let mut block = self.instructions_required()?;
154 let level = self.instructions_level()?;
155 let old_views: Vec<String> = block.steps().filter_map(|(_, s)| s.view.clone()).collect();
156 wizard::propose(&mut block, &level);
157 let mut state = self.pmi_state();
158 state.views.retain(|view| !old_views.contains(&view.id));
159 self.instructions_step_views(&mut block, &mut state)?;
160 block.validate()?;
161 let steps = block.step_count();
162 self.history.set_document_block(BLOCK_KEY, Some(block.to_value()), None);
163 let pmi = if state.is_empty() { None } else { serde_json::to_value(&state).ok() };
164 self.history.set_pmi_block_no_undo(pmi);
165 self.rerun_history();
166 Ok(steps)
167 }
168
169 pub fn instructions_run_wizard_when_built(&mut self) -> Result<(), String> {
174 self.instructions_required()?;
175 if self.run_pending() {
176 self.instructions_wizard_pending = true;
177 return Ok(());
178 }
179 self.instructions_run_wizard().map(|_| ())
180 }
181
182 pub fn instructions_wizard_pending(&self) -> bool {
184 self.instructions_wizard_pending
185 }
186
187 pub fn instructions_restructure<T>(&mut self, edit: impl FnOnce(&mut Instructions) -> Result<T, String>) -> Result<T, String> {
194 self.instructions_restructure_with_views(|block, _| edit(block))
195 }
196
197 fn instructions_restructure_with_views<T>(
200 &mut self,
201 edit: impl FnOnce(&mut Instructions, &mut brep_kernel::PmiState) -> Result<T, String>,
202 ) -> Result<T, String> {
203 let mut block = self.instructions_required()?;
204 let before: Vec<String> = block.steps().filter_map(|(_, s)| s.view.clone()).collect();
205 let mut state = self.pmi_state();
206 let out = edit(&mut block, &mut state)?;
207 block.validate()?;
208 let kept: Vec<String> = block.steps().filter_map(|(_, s)| s.view.clone()).collect();
209 state.views.retain(|view| !before.contains(&view.id) || kept.contains(&view.id));
210 self.instructions_step_views(&mut block, &mut state)?;
211 self.history.set_document_block(BLOCK_KEY, Some(block.to_value()), None);
212 let pmi = if state.is_empty() { None } else { serde_json::to_value(&state).ok() };
213 self.history.set_pmi_block_no_undo(pmi);
214 self.rerun_history();
215 Ok(out)
216 }
217
218 pub(super) fn instructions_fold_retarget(&self, incoming: &mut Value) -> Result<bool, String> {
227 let Ok((id, part)) = self.instructions_wrapper() else { return Ok(false) };
228 let old_entry = &self.history.parts_library()[part.as_str()];
229 let new_entry = &incoming["partsLibrary"][part.as_str()];
230 if new_entry.is_null() || old_entry["document"] == new_entry["document"] {
231 return Ok(false);
232 }
233 let Some(mut block) = incoming.get(BLOCK_KEY).filter(|b| !b.is_null()).map(Instructions::from_value).transpose()? else {
234 return Ok(false);
235 };
236 if block.retarget.is_some() {
237 return Err("the instructions' re-target waits for approval: approve it in the Instructions pane, then update again".into());
238 }
239 let old = source::level_from_document(&old_entry["document"], &id, "", &part);
240 let new = source::level_from_document(&new_entry["document"], &id, "", &part);
241 let text = |key: &str, or: &str| new_entry[key].as_str().filter(|v| !v.is_empty()).unwrap_or(or).to_string();
242 let to = Target {
243 source_key: text("sourceKey", &block.target.source_key),
244 signature: text("sourceSignature", &block.target.signature),
245 ..block.target.clone()
246 };
247 let diff = retarget::diff(&old, &new);
248 let changed = !diff.is_empty();
249 if changed {
250 let before: Vec<String> = block.steps().filter_map(|(_, s)| s.view.clone()).collect();
251 retarget::apply(&mut block, &new, &diff, to)?;
252 let kept: Vec<String> = block.steps().filter_map(|(_, s)| s.view.clone()).collect();
254 if let Some(views) = incoming["pmi"]["views"].as_array_mut() {
255 views.retain(|view| view["id"].as_str().is_none_or(|id| !before.iter().any(|b| b == id) || kept.iter().any(|k| k == id)));
256 }
257 } else {
258 block.target = to;
259 }
260 block.validate()?;
261 incoming[BLOCK_KEY] = block.to_value();
262 Ok(changed)
263 }
264
265 pub(super) fn instructions_refresh_views(&mut self) -> Result<(), String> {
270 let mut block = self.instructions_required()?;
271 let mut state = self.pmi_state();
272 self.instructions_step_views(&mut block, &mut state)?;
273 self.history.set_document_block_no_undo(BLOCK_KEY, Some(block.to_value()));
274 let pmi = if state.is_empty() { None } else { serde_json::to_value(&state).ok() };
275 self.history.set_pmi_block_no_undo(pmi);
276 self.rerun_history();
277 Ok(())
278 }
279
280 pub fn instructions_retarget_approve(&mut self) -> Result<Target, String> {
284 self.instructions_restructure(retarget::approve)
285 }
286
287 pub fn instructions_retarget_reject(&mut self) -> Result<Target, String> {
296 let block = self.instructions_required()?;
297 let from = block.retarget.as_ref().ok_or("no re-target is waiting for approval")?.from.clone();
298 if !self.history.can_undo() {
299 return Err("the re-target came with the file: nothing in this session takes it back; approve it, or update again later".into());
300 }
301 let fold_is_newest = self
302 .history
303 .undo_top_block(BLOCK_KEY)
304 .is_some_and(|before| before.get("retarget").is_none_or(Value::is_null));
305 if !fold_is_newest {
306 return Err("edits followed the re-target: undo them first, or approve it".into());
307 }
308 self.undo();
309 Ok(from)
310 }
311
312 pub fn begin_ref_select_for_instructions(&mut self, step_id: &str, field: InstructionsRefField, seed: Option<&str>) {
318 let restore_index = self.history.rollback();
319 let filter = vec!["SOLID".to_string()];
320 let label = match field {
321 InstructionsRefField::Arrow(_) => "Part the arrow moves".to_string(),
322 InstructionsRefField::Callout(_) => "Part the callout points at".to_string(),
323 };
324 let path = match field {
325 InstructionsRefField::Arrow(index) => vec!["arrows".to_string(), index.to_string(), "occurrence".to_string()],
326 InstructionsRefField::Callout(index) => vec!["callouts".to_string(), index.to_string(), "occurrence".to_string()],
327 };
328 let names = seed.and_then(|occurrence| self.instructions_occurrence_solid(occurrence)).into_iter().collect();
329 self.selection_filter = crate::engine_state::selection_filter::SelectionFilter::from_ref_filter(&filter);
330 self.ref_select = Some(super::RefSelectState {
331 feature_id: step_id.to_string(),
332 path,
333 label,
334 filter,
335 multiple: false,
336 names,
337 restore_index,
338 target: RefSelectTarget::Instructions { field },
339 picks: 0,
340 claimed: false,
341 finish_requested: false,
342 });
343 self.sync_ref_select_emphasis();
344 }
345
346 pub fn instructions_occurrence_of_solid(&self, solid: &str) -> Result<String, String> {
350 let (occurrence, _) = solid.rsplit_once(':').ok_or_else(|| format!("'{solid}' is not a part of the assembly"))?;
351 let level = self.instructions_level()?;
352 if !wizard::flatten(&level).iter().any(|(_, c)| c.path == occurrence) {
353 return Err(format!("'{solid}' is not a part of the assembly"));
354 }
355 Ok(occurrence.to_string())
356 }
357
358 pub fn instructions_occurrence_solid(&self, occurrence: &str) -> Option<String> {
361 let prefix = format!("{occurrence}:");
362 self.scene.solids().iter().map(|solid| solid.name.clone()).find(|name| name.starts_with(&prefix))
363 }
364
365 pub(crate) fn instructions_commit_ref(&mut self, step_id: &str, field: InstructionsRefField, solid: &str) -> Result<(), String> {
370 let occurrence = self.instructions_occurrence_of_solid(solid)?;
371 let step_id = step_id.to_string();
372 self.instructions_edit(None, |block| {
373 let step = block.step_mut(&step_id).ok_or_else(|| format!("no step '{step_id}'"))?;
374 match field {
375 InstructionsRefField::Arrow(index) => {
376 if !step.adds.iter().any(|add| add.occurrences.contains(&occurrence)) {
377 return Err(format!("{occurrence} is not a part this step adds: an arrow moves one of the step's own parts"));
378 }
379 let arrow = step.arrows.get_mut(index).ok_or_else(|| format!("no arrow {index}"))?;
380 arrow.occurrence = occurrence;
381 }
382 InstructionsRefField::Callout(index) => {
383 let callout = step.callouts.get_mut(index).ok_or_else(|| format!("no callout {index}"))?;
384 callout.occurrence = Some(occurrence);
385 }
386 }
387 Ok(())
388 })?;
389 self.history.break_coalescing();
390 Ok(())
391 }
392
393 pub fn instructions_new_step(&mut self, after: Option<&str>) -> Result<String, String> {
396 self.instructions_restructure(|block| edit::insert_step(block, after))
397 }
398
399 pub fn instructions_add_parts(&mut self, step_id: &str, occurrences: &[String]) -> Result<edit::AddReport, String> {
405 let (wrapper, _) = self.instructions_wrapper()?;
406 let level = self.instructions_level()?;
407 let flat = wizard::flatten(&level);
408 let mut components = Vec::new();
409 for occurrence in occurrences {
410 if *occurrence == wrapper {
411 return Err("that row is the whole assembly: drag a part row into the step".into());
412 }
413 let component = flat
414 .iter()
415 .find(|(_, c)| &c.path == occurrence)
416 .map(|(_, c)| *c)
417 .ok_or_else(|| format!("'{occurrence}' is not an occurrence of the assembly"))?;
418 components.push(component);
419 }
420 if self.instructions_required()?.step(step_id).is_none() {
421 return Err(format!("no step '{step_id}'"));
422 }
423 self.instructions_restructure(|block| edit::add_parts(block, step_id, &components))
424 }
425
426 pub fn instructions_add_part(&mut self, step_id: &str, part: &str) -> Result<edit::AddReport, String> {
431 let level = self.instructions_level()?;
432 let occurrences: Vec<String> = level.components.iter().filter(|c| c.part == part || c.part_key() == part).map(|c| c.path.clone()).collect();
433 if occurrences.is_empty() {
434 return Err(format!("the assembly has no part '{part}'"));
435 }
436 self.instructions_add_parts(step_id, &occurrences)
437 }
438
439 pub fn instructions_remove_add(&mut self, step_id: &str, add_index: usize) -> Result<(), String> {
442 self.instructions_restructure(|block| edit::remove_add(block, step_id, add_index).map(drop))
443 }
444
445 pub fn instructions_nest_step(&mut self, step_id: &str, parent: Option<&str>, index: usize) -> Result<(), String> {
449 self.instructions_restructure(|block| edit::nest_step(block, step_id, parent, index))
450 }
451
452 pub fn instructions_unplanned(&self) -> Vec<(String, String)> {
455 let Ok(Some(block)) = self.instructions() else { return Vec::new() };
456 let Ok(level) = self.instructions_level() else { return Vec::new() };
457 edit::unplanned(&block, &level).into_iter().map(|c| (c.path.clone(), c.part.clone())).collect()
458 }
459
460 pub fn instructions_capture_camera(&mut self, step_id: &str) -> Result<(), String> {
464 let snapshot = self.snapshot_camera();
465 self.instructions_restructure_with_views(|block, state| {
466 let step = block.step_mut(step_id).ok_or_else(|| format!("no step '{step_id}'"))?;
467 step.camera_captured = true;
468 if let Some(view) = step.view.as_deref().and_then(|id| state.find_view_mut(id)) {
469 if let Some(section) = view.display.section.as_mut() {
473 section.freeze(view.camera.as_ref());
474 }
475 view.camera = Some(snapshot);
476 }
477 Ok(())
478 })
479 }
480
481 pub fn instructions_reset_view(&mut self, step_id: &str) -> Result<(), String> {
484 self.instructions_restructure(|block| {
485 block.step_mut(step_id).ok_or_else(|| format!("no step '{step_id}'"))?.camera_captured = false;
486 Ok(())
487 })
488 }
489
490 pub(super) fn instructions_step_of_view(&self, view_id: &str) -> Option<String> {
492 let block = self.instructions().ok().flatten()?;
493 let step = block.steps().find(|(_, step)| step.view.as_deref() == Some(view_id)).map(|(_, step)| step.id.clone());
494 step
495 }
496
497 fn instructions_step_views(&mut self, block: &mut Instructions, state: &mut brep_kernel::PmiState) -> Result<(), String> {
507 self.pmi_deactivate_view();
508 let camera = self.camera.clone();
509 let solids: Vec<String> = self.scene.solids().iter().map(|s| s.name.clone()).collect();
510 let boxes: std::collections::HashMap<String, crate::camera::Aabb> =
511 self.scene.solids().iter().map(|s| (s.name.clone(), s.bbox)).collect();
512 fn parents_first(steps: &[brep_instructions::model::Step], out: &mut Vec<String>) {
513 for step in steps {
514 out.push(step.id.clone());
515 parents_first(&step.steps, out);
516 }
517 }
518 let mut ids = Vec::new();
519 parents_first(&block.steps, &mut ids);
520 for step_id in &ids {
521 let presentation = edit::presentation(block, step_id, &solids)?;
522 let number = block.step_number(step_id).unwrap_or(0);
523 let step = block.step(step_id).expect("listed step");
524 let existing = step.view.as_deref().and_then(|id| state.find_view(id)).cloned();
525 let parent_camera = block
526 .parent_id(step_id)
527 .flatten()
528 .and_then(|parent| block.step(parent))
529 .and_then(|parent| parent.view.as_deref())
530 .and_then(|id| state.find_view(id))
531 .and_then(|view| view.camera.clone());
532 let slides = block.template.style == Style::LineArt && step.arrows.is_empty();
535 let camera_now = match existing.as_ref().and_then(|view| view.camera.clone()).filter(|_| step.camera_captured) {
536 Some(kept) => kept,
537 None => self.generated_step_camera(&presentation, &boxes, parent_camera.as_ref(), slides),
538 };
539 let mut annotations: Vec<PmiAnnotation> = existing
542 .as_ref()
543 .map(|view| {
544 view.annotations
545 .iter()
546 .filter(|a| !(a.kind == "balloon" && a.params.get("occurrence").is_some()) && !a.is_baseline_hide())
547 .cloned()
548 .collect()
549 })
550 .unwrap_or_default();
551 let baseline_id = existing
555 .as_ref()
556 .and_then(|view| view.annotations.iter().find(|a| a.is_baseline_hide()))
557 .map(|a| a.id().to_string())
558 .filter(|id| !id.is_empty());
559 let view_id = match &existing {
560 Some(view) => view.id.clone(),
561 None => state.next_id("VIEW"),
562 };
563 for add in step.adds.iter().filter(|a| !a.find_number.is_empty()) {
564 let Some(occurrence) = add.occurrences.first() else { continue };
565 let prefix = format!("{occurrence}:");
566 let Some(member) = solids.iter().find(|name| name.starts_with(&prefix)) else { continue };
567 annotations.push(PmiAnnotation {
568 plugin_replay: None,
569 persistent_data: Value::Null,
570 kind: "balloon".into(),
571 enabled: true,
572 params: json!({ "id": state.next_id("BAL"), "target": member, "occurrence": occurrence }),
573 label_world: None,
574 });
575 }
576 if !presentation.hidden.is_empty() {
577 let id = baseline_id.unwrap_or_else(|| brep_kernel::baseline_hide_id(&view_id));
578 annotations.push(PmiAnnotation::hide_step(&id, &presentation.hidden, true));
579 }
580 let display = existing.as_ref().map(|view| view.display.clone()).unwrap_or_else(|| PmiDisplay::new(12.0, false, None));
581 let view = PmiView {
582 id: view_id.clone(),
583 name: format!("Step {number} · {}", step.title),
584 camera: Some(camera_now),
585 display,
586 annotations,
587 };
588 match state.find_view_mut(&view_id) {
589 Some(slot) => *slot = view,
590 None => state.views.push(view),
591 }
592 block.step_mut(step_id).expect("listed step").view = Some(view_id);
593 }
594 self.camera = camera;
595 Ok(())
596 }
597
598 fn generated_step_camera(
607 &mut self,
608 presentation: &edit::Presentation,
609 boxes: &std::collections::HashMap<String, crate::camera::Aabb>,
610 parent: Option<&PmiCamera>,
611 slides: bool,
612 ) -> PmiCamera {
613 let union = |names: &[String]| {
614 let mut bbox = crate::camera::Aabb::empty();
615 for name in names {
616 if let Some(solid) = boxes.get(name) {
617 bbox.union(solid);
618 }
619 }
620 bbox
621 };
622 let mut shown = union(&presentation.placed);
623 shown.union(&union(&presentation.adding));
624 let frame = shown.clone();
625 match parent {
626 Some(parent) => {
627 let (kind, scale) = match parent.projection {
628 PmiProjection::Orthographic { half_height } => ("orthographic", half_height),
629 PmiProjection::Perspective { fov_y_deg } => ("perspective", fov_y_deg),
630 };
631 let json = json!({ "kind": kind, "eye": parent.eye, "target": parent.target, "up": parent.up, "scale": scale }).to_string();
632 let _ = self.camera.apply_state_json(&json);
633 }
634 None => {
635 self.camera.projection = crate::view::Projection::Orthographic { half_height: 10.0 };
638 self.camera.standard_view("iso");
639 }
640 }
641 let mut frame = frame;
642 if slides {
643 if let Some(offset) = default_start_offset(presentation, boxes, self.camera.up) {
644 let mut start = union(&presentation.adding);
645 start.min = std::array::from_fn(|k| start.min[k] + offset[k]);
646 start.max = std::array::from_fn(|k| start.max[k] + offset[k]);
647 frame.union(&start);
648 }
649 }
650 self.camera.width = GENERATED_VIEWPORT[0];
651 self.camera.height = GENERATED_VIEWPORT[1];
652 if !frame.is_empty() {
653 self.camera.zoom_to_fit(&frame, 1.25);
654 }
655 if matches!(self.camera.projection, crate::view::Projection::Orthographic { .. }) {
659 let size = if shown.is_empty() { [0.0; 3] } else { shown.size() };
663 let diagonal = (size[0] * size[0] + size[1] * size[1] + size[2] * size[2]).sqrt();
664 let distance = (4.0 * diagonal).max(10.0);
665 let (eye, target) = (self.camera.eye, self.camera.target);
666 let direction = [eye[0] - target[0], eye[1] - target[1], eye[2] - target[2]];
667 let length = (direction[0] * direction[0] + direction[1] * direction[1] + direction[2] * direction[2]).sqrt();
668 if length > 1e-12 {
669 self.camera.eye = [
670 target[0] + direction[0] / length * distance,
671 target[1] + direction[1] / length * distance,
672 target[2] + direction[2] / length * distance,
673 ];
674 }
675 }
676 self.snapshot_camera()
677 }
678
679 pub fn instructions_show_step(&mut self, step_id: &str) -> Result<(), String> {
681 let block = self.instructions_required()?;
682 let step = block.step(step_id).ok_or_else(|| format!("no step '{step_id}'"))?;
683 let view = step.view.clone().ok_or_else(|| format!("step '{step_id}' has no view: run the wizard or pick one"))?;
684 self.pmi_activate_view(&view)
685 }
686
687 pub(crate) fn instructions_refresh_ghosts(&mut self) {
699 if !self.is_instructions() {
700 return;
701 }
702 let shown = self.instructions_shown_presentation();
703 let line_art = shown.is_some() && self.instructions().ok().flatten().is_some_and(|block| block.template.style == Style::LineArt);
707 let all_placed = shown.map(|presentation| presentation.placed).unwrap_or_default();
708 let look = line_art.then(|| crate::scene::LineArtLook { placed: all_placed.iter().cloned().collect() });
709 if self.scene.line_art != look {
710 self.scene.line_art = look;
711 self.dirty = true;
712 }
713 let placed = if line_art { Vec::new() } else { all_placed };
714 let gone: Vec<String> =
715 self.context_scene.solids().iter().map(|ghost| ghost.name.clone()).filter(|name| !placed.contains(name)).collect();
716 for name in gone {
717 self.context_scene.remove_solid(&name);
718 self.scene.set_visible(&name, true);
719 self.dirty = true;
720 }
721 for name in placed {
722 let Some(solid) = self.scene.solid(&name) else { continue };
723 if !solid.visible && self.context_scene.solid(&name).is_none() {
726 continue;
727 }
728 let mut ghost = solid.clone();
729 ghost.visible = true;
730 ghost.source_handle = 0;
731 ghost.vertices.clear();
732 if self.context_scene.solid(&name) != Some(&ghost) {
733 self.context_scene.insert_solid(ghost);
734 self.dirty = true;
735 }
736 if solid.visible {
737 self.scene.set_visible(&name, false);
738 self.dirty = true;
739 }
740 }
741 if !self.context_scene.solids().is_empty() && self.context_scene.tint != Some(GHOST_RGBA) {
742 self.context_scene.tint = Some(GHOST_RGBA);
743 self.dirty = true;
744 }
745 }
746
747 pub fn instructions_step_arrows(&self, block: &Instructions, step_id: &str) -> Vec<Arrow> {
755 let Some(step) = block.step(step_id) else { return Vec::new() };
756 if !step.arrows.is_empty() || block.template.style != Style::LineArt {
757 return step.arrows.clone();
758 }
759 let pmi = self.pmi_state();
760 let Some(up) = step.view.as_deref().and_then(|id| pmi.find_view(id)).and_then(|view| view.camera.as_ref()).map(|camera| camera.up) else {
761 return Vec::new();
762 };
763 let solids: Vec<String> = self.scene.solids().iter().map(|s| s.name.clone()).collect();
764 let Ok(presentation) = edit::presentation(block, step_id, &solids) else { return Vec::new() };
765 let boxes: std::collections::HashMap<String, crate::camera::Aabb> =
766 self.scene.solids().iter().map(|s| (s.name.clone(), s.bbox)).collect();
767 default_arrows(step, &presentation, &boxes, up)
768 }
769
770 pub fn instructions_play(&mut self, to: PlayTo) -> Result<String, String> {
775 let block = self.instructions_required()?;
776 let order: Vec<String> = block.steps().map(|(_, step)| step.id.clone()).collect();
777 if order.is_empty() {
778 return Err("there are no steps yet: run the wizard first".into());
779 }
780 let shown = self.instructions_shown_step();
781 let at = shown.as_ref().and_then(|id| order.iter().position(|step| step == id));
782 let step = match to {
783 PlayTo::First => order[0].clone(),
784 PlayTo::Next => order[at.map_or(0, |i| (i + 1).min(order.len() - 1))].clone(),
785 PlayTo::Back => order[at.map_or(0, |i| i.saturating_sub(1))].clone(),
786 PlayTo::Replay => shown.unwrap_or_else(|| order[0].clone()),
787 PlayTo::Step(id) => order.iter().find(|step| **step == id).cloned().ok_or_else(|| format!("no step '{id}'"))?,
788 };
789 self.instructions_show_step(&step)?;
790 let view = self.pmi_active_view().map(str::to_string).unwrap_or_default();
791 let solids: Vec<String> = self.scene.solids().iter().map(|solid| solid.name.clone()).collect();
792 let presentation = edit::presentation(&block, &step, &solids)?;
793 let under = |path: &str, occurrence: &str| path == occurrence || (path.starts_with(occurrence) && path[occurrence.len()..].starts_with(':'));
794 let arrows = self.instructions_step_arrows(&block, &step);
795 let moves: Vec<(Vec<String>, [f64; 3])> = block
796 .step(&step)
797 .map(|_| {
798 arrows
799 .iter()
800 .map(|arrow| {
801 let names: Vec<String> = presentation.adding.iter().filter(|name| under(name, &arrow.occurrence)).cloned().collect();
804 (names, arrow.start_offset())
805 })
806 .filter(|(names, offset)| !names.is_empty() && offset.iter().any(|v| *v != 0.0))
807 .collect()
808 })
809 .unwrap_or_default();
810 self.instructions_insertion = (!moves.is_empty()).then(|| Insertion { view, moves, fraction: 0.0, held: false });
811 self.pmi_apply_explode();
812 self.refresh_pmi_overlay();
813 Ok(step)
814 }
815
816 pub fn instructions_insertion_tick(&mut self, dt: f64) -> bool {
819 let Some(insertion) = self.instructions_insertion.as_mut() else { return false };
820 if self.pmi_active_view.as_deref() != Some(insertion.view.as_str()) {
821 self.instructions_insertion = None;
822 return false;
823 }
824 if insertion.held {
825 return false;
826 }
827 insertion.fraction = (insertion.fraction + dt.max(0.0) / INSERTION_SECONDS).min(1.0);
828 let done = insertion.fraction >= 1.0;
829 if done {
830 self.instructions_insertion = None;
831 }
832 self.pmi_apply_explode();
833 self.refresh_pmi_overlay();
834 !done
835 }
836
837 pub(crate) fn instructions_insertion_poses(&self) -> Vec<(Vec<String>, [f64; 3], [f64; 3], [f64; 3], [f64; 3])> {
841 let Some(insertion) = &self.instructions_insertion else { return Vec::new() };
842 if self.pmi_active_view.as_deref() != Some(insertion.view.as_str()) {
843 return Vec::new();
844 }
845 let left = 1.0 - insertion.fraction;
846 insertion
847 .moves
848 .iter()
849 .map(|(names, offset)| (names.clone(), offset.map(|v| v * left), [0.0; 3], [1.0; 3], [0.0; 3]))
850 .collect()
851 }
852
853 pub(crate) fn instructions_insertion_arrows(&self) -> Vec<([f64; 3], [f64; 3])> {
859 let Some(insertion) = &self.instructions_insertion else { return Vec::new() };
860 if self.pmi_active_view.as_deref() != Some(insertion.view.as_str()) {
861 return Vec::new();
862 }
863 let left = 1.0 - insertion.fraction;
864 insertion
865 .moves
866 .iter()
867 .filter_map(|(names, offset)| {
868 let mut bbox = crate::camera::Aabb::empty();
869 for name in names {
870 let home = self.pmi_explode_originals.get(name).or_else(|| self.scene.solid(name));
871 if let Some(display) = home {
872 bbox.union(&display.bbox);
873 }
874 }
875 self.outline_arrow(&bbox, offset.map(|v| v * left))
876 })
877 .collect()
878 }
879
880 pub fn instructions_hold_insertion(&mut self, fraction: f64) {
883 let Some(insertion) = self.instructions_insertion.as_mut() else { return };
884 insertion.fraction = if fraction.is_finite() { fraction.clamp(0.0, 1.0) } else { 0.0 };
885 insertion.held = true;
886 self.pmi_apply_explode();
887 self.refresh_pmi_overlay();
888 }
889
890 pub(crate) fn outline_arrow(&self, home: &crate::camera::Aabb, now: [f64; 3]) -> Option<([f64; 3], [f64; 3])> {
896 let length = (now[0] * now[0] + now[1] * now[1] + now[2] * now[2]).sqrt();
897 if home.is_empty() || !(length > 1e-9) {
898 return None;
899 }
900 let d = now.map(|v| -v / length);
902 let (c, size) = (home.center(), home.size());
903 let h = 0.5 * (size[0] * d[0].abs() + size[1] * d[1].abs() + size[2] * d[2].abs());
904 let (right, up, _) = self.camera.basis();
905 let screen = |p: [f64; 3]| [crate::geometry3d::dot3(p, right), crate::geometry3d::dot3(p, up)];
906 let sd = screen(d);
907 let sl = (sd[0] * sd[0] + sd[1] * sd[1]).sqrt();
908 let mut lead = h;
909 if sl > 1e-9 {
910 let u = [sd[0] / sl, sd[1] / sl];
911 let along = |p: [f64; 3]| {
912 let s = screen(p);
913 s[0] * u[0] + s[1] * u[1]
914 };
915 let reach = (0..8)
916 .map(|i| {
917 along([
918 if i & 1 == 0 { home.min[0] } else { home.max[0] },
919 if i & 2 == 0 { home.min[1] } else { home.max[1] },
920 if i & 4 == 0 { home.min[2] } else { home.max[2] },
921 ])
922 })
923 .fold(f64::NEG_INFINITY, f64::max);
924 lead = ((reach - along(c)) / sl).max(h);
925 }
926 let seat: [f64; 3] = std::array::from_fn(|k| c[k] + d[k] * lead);
927 Some((std::array::from_fn(|k| seat[k] + now[k]), seat))
928 }
929
930 pub fn instructions_insertion_fraction(&self) -> Option<f64> {
932 self.instructions_insertion.as_ref().map(|insertion| insertion.fraction)
933 }
934
935 pub fn instructions_shown_step(&self) -> Option<String> {
937 let active = self.pmi_active_view()?;
938 let block = self.instructions().ok().flatten()?;
939 let step = block.steps().find(|(_, step)| step.view.as_deref() == Some(active)).map(|(_, step)| step.id.clone());
940 step
941 }
942
943 fn instructions_shown_presentation(&self) -> Option<edit::Presentation> {
946 let active = self.pmi_active_view()?;
947 let block = self.instructions().ok().flatten()?;
948 let (_, step) = block.steps().find(|(_, step)| step.view.as_deref() == Some(active))?;
949 let solids: Vec<String> = self.scene.solids().iter().map(|solid| solid.name.clone()).collect();
950 edit::presentation(&block, &step.id, &solids).ok()
951 }
952
953 pub fn instructions_state_json(&self) -> Value {
956 match self.instructions() {
957 Ok(Some(block)) => {
958 let shown = self.pmi_active_view().and_then(|view| {
959 block.steps().find(|(_, s)| s.view.as_deref() == Some(view)).map(|(_, s)| s.id.clone())
960 });
961 json!({
962 "instructions": block.to_value(),
963 "steps": block.step_count(),
964 "wizardPending": self.instructions_wizard_pending,
965 "viewsPending": self.instructions_views_pending,
966 "shownStep": shown,
967 "ghosted": self.context_scene.solids().iter().map(|ghost| ghost.name.as_str()).collect::<Vec<_>>(),
968 "insertion": self.instructions_insertion_fraction(),
969 "emptySteps": edit::empty_steps(&block),
970 "unplanned": self.instructions_unplanned().into_iter().map(|(occurrence, part)| json!({ "occurrence": occurrence, "part": part })).collect::<Vec<_>>(),
971 "wrapper": self.instructions_wrapper().ok().map(|(id, part)| json!({ "id": id, "part": part })),
972 })
973 }
974 Ok(None) => json!({ "instructions": null }),
975 Err(error) => json!({ "instructions": null, "error": error }),
976 }
977 }
978}
979
980pub(crate) fn default_start_offset(
987 presentation: &edit::Presentation,
988 boxes: &std::collections::HashMap<String, crate::camera::Aabb>,
989 up: [f64; 3],
990) -> Option<[f64; 3]> {
991 if presentation.placed.is_empty() || presentation.adding.is_empty() {
992 return None;
993 }
994 let length = (up[0] * up[0] + up[1] * up[1] + up[2] * up[2]).sqrt();
995 if !(length > 1e-12) {
996 return None;
997 }
998 let up = up.map(|v| v / length);
999 let union = |names: &[String]| {
1000 let mut bbox = crate::camera::Aabb::empty();
1001 for name in names {
1002 if let Some(solid) = boxes.get(name) {
1003 bbox.union(solid);
1004 }
1005 }
1006 bbox
1007 };
1008 let adding = union(&presentation.adding);
1009 if adding.is_empty() {
1010 return None;
1011 }
1012 let mut shown = union(&presentation.placed);
1013 shown.union(&adding);
1014 let along = |b: &crate::camera::Aabb| {
1015 let size = b.size();
1016 size[0] * up[0].abs() + size[1] * up[1].abs() + size[2] * up[2].abs()
1017 };
1018 let size = shown.size();
1019 let diagonal = (size[0] * size[0] + size[1] * size[1] + size[2] * size[2]).sqrt();
1020 let distance = (2.0 * along(&adding)).max(diagonal / 3.0);
1021 (distance.is_finite() && distance > 0.0).then(|| up.map(|v| v * distance))
1022}
1023
1024pub(crate) fn default_arrows(
1027 step: &Step,
1028 presentation: &edit::Presentation,
1029 boxes: &std::collections::HashMap<String, crate::camera::Aabb>,
1030 up: [f64; 3],
1031) -> Vec<Arrow> {
1032 let Some(offset) = default_start_offset(presentation, boxes, up) else { return Vec::new() };
1033 let distance = (offset[0] * offset[0] + offset[1] * offset[1] + offset[2] * offset[2]).sqrt();
1034 let mut occurrences = Vec::new();
1035 step.occurrences_with_descendants(&mut occurrences);
1036 occurrences
1038 .iter()
1039 .filter(|occurrence| !occurrences.iter().any(|other| other != *occurrence && occurrence.starts_with(&format!("{other}:"))))
1040 .map(|occurrence| Arrow { occurrence: occurrence.to_string(), direction: offset.map(|v| -v / distance), distance })
1041 .collect()
1042}