brep_app/panels/
interference.rs1use crate::automation::hit_keys::HitKeyDoc;
25use brep_render::assembly_status;
26use brep_render::engine_state::{EngineState, InterferencePair, InterferenceReport};
27use eframe::egui;
28use serde_json::Value;
29use std::collections::HashMap;
30
31fn pair_label(pair: &InterferencePair) -> String {
34 let mut label = format!(
35 "{} \u{00d7} {} \u{2014} {} mm\u{00b3}",
36 pair.a,
37 pair.b,
38 super::info_windows::num(pair.volume)
39 );
40 let hidden: Vec<&str> = [
41 pair.a_hidden.then_some(pair.a.as_str()),
42 pair.b_hidden.then_some(pair.b.as_str()),
43 ]
44 .into_iter()
45 .flatten()
46 .collect();
47 if !hidden.is_empty() {
48 label.push_str(&format!(" (hidden: {})", hidden.join(", ")));
49 }
50 label
51}
52
53fn status_color(status: &str) -> egui::Color32 {
55 let [r, g, b] = assembly_status::status_color_rgb(status);
56 egui::Color32::from_rgb(r, g, b)
57}
58
59#[derive(Default)]
63pub struct InterferenceWindow {
64 open: bool,
65 report: Option<InterferenceReport>,
66 hits: HashMap<String, egui::Rect>,
68}
69
70impl InterferenceWindow {
71 pub fn new() -> Self {
72 Self::default()
73 }
74
75 pub fn open_and_run(&mut self, state: &mut EngineState) {
77 self.report = Some(state.interference_check());
78 self.open = true;
79 }
80
81 pub fn show(&mut self, ctx: &egui::Context, state: &mut EngineState) {
83 self.hits.clear();
84 if !self.open {
85 return;
86 }
87 let mut open = true;
88 egui::Window::new("Interference")
89 .id(egui::Id::new("brep-interference-window"))
90 .open(&mut open)
91 .movable(true)
92 .resizable(true)
93 .default_size([360.0, 320.0])
94 .default_pos([860.0, 80.0])
95 .show(ctx, |ui| {
96 egui::ScrollArea::vertical()
97 .auto_shrink([false, false])
98 .show(ui, |ui| self.body(ui, state));
99 });
100 self.open = open;
101 }
102
103 fn body(&mut self, ui: &mut egui::Ui, state: &mut EngineState) {
104 ui.horizontal(|ui| {
106 let rerun = ui.button("Re-run");
107 self.hits.insert("interference:rerun".into(), rerun.rect);
108 if rerun.clicked() {
109 self.report = Some(state.interference_check());
110 }
111 if let Some(report) = &self.report {
112 ui.weak(format!(
113 "{} components \u{00b7} {} pairs \u{00b7} {} boolean{}",
114 report.component_count,
115 report.pair_total,
116 report.booleans_run,
117 if report.booleans_run == 1 { "" } else { "s" }
118 ));
119 }
120 });
121 ui.separator();
122
123 let Some(report) = self.report.clone() else {
124 ui.weak("Run the check from the toolbar.");
125 return;
126 };
127 if report.component_count < 2 {
128 ui.weak("Needs at least two components.");
129 return;
130 }
131
132 if report.pairs.is_empty() {
133 ui.colored_label(
136 status_color("satisfied"),
137 format!(
138 "\u{2713} No interference \u{2014} {} pair{} checked",
139 report.pair_total,
140 if report.pair_total == 1 { "" } else { "s" }
141 ),
142 );
143 } else {
144 for pair in &report.pairs {
145 let row = ui.selectable_label(
148 false,
149 egui::RichText::new(pair_label(pair)).color(status_color("error")),
150 );
151 self.hits
152 .insert(format!("interference:pair:{}x{}", pair.a, pair.b), row.rect);
153 if row.clicked() {
154 state.select_components(&[pair.a.clone(), pair.b.clone()]);
155 }
156 }
157 }
158
159 if !report.unverified.is_empty() {
161 ui.add_space(4.0);
162 ui.label("Not verified (boolean refused):");
163 for line in &report.unverified {
164 ui.colored_label(status_color("unsupported-selection"), line);
165 }
166 }
167 if !report.skipped.is_empty() {
168 ui.add_space(4.0);
169 ui.label("Not checked:");
170 for line in &report.skipped {
171 ui.weak(line);
172 }
173 }
174 }
175
176 #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
179 pub fn state_json(&self) -> String {
180 let report = self.report.as_ref().map(|report| {
181 serde_json::json!({
182 "componentCount": report.component_count,
183 "pairTotal": report.pair_total,
184 "booleansRun": report.booleans_run,
185 "pairs": report
186 .pairs
187 .iter()
188 .map(|pair| {
189 serde_json::json!({
190 "a": pair.a,
191 "b": pair.b,
192 "volume": pair.volume,
193 "aHidden": pair.a_hidden,
194 "bHidden": pair.b_hidden,
195 "label": pair_label(pair),
196 })
197 })
198 .collect::<Vec<_>>(),
199 "skipped": report.skipped,
200 "unverified": report.unverified,
201 })
202 });
203 serde_json::json!({
204 "open": self.open,
205 "report": report.unwrap_or(Value::Null),
206 })
207 .to_string()
208 }
209
210 #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
213 pub fn hits_json(&self) -> String {
214 crate::automation::hit_rects::hits_json(&self.hits)
215 }
216}
217
218pub static HIT_KEYS: &[HitKeyDoc] = &[
222 HitKeyDoc { panel: "interference", prefix: "interference:rerun", meaning: "rerun the interference check", command: Some("interference_check") },
223 HitKeyDoc { panel: "interference", prefix: "interference:", meaning: "an interference result row", command: None },
224];