brep_render/engine_state/
pmi_overlay.rs1use super::*;
9use brep_kernel::{pmi_present, pmi_type, PmiLayoutStyle, PmiStatus};
10
11const PMI_OVERLAY_GROUP: &str = "pmi-overlay";
12pub(crate) const ARROW_PX: f64 = 12.0;
14pub(crate) const TEXT_PX_AT_12PT: f64 = 11.0;
17const PMI_RGB: [f32; 3] = [0.98, 0.78, 0.22];
20const ERROR_RGB: [f32; 3] = [0.97, 0.32, 0.29];
22const DISABLED_RGB: [f32; 3] = [0.55, 0.55, 0.55];
24pub(crate) const LIVE_HIDDEN_MESSAGE: &str = "target hidden in the viewport: show it, or update the view's visibility";
27
28#[derive(Debug, Clone, PartialEq, serde::Serialize)]
31#[serde(rename_all = "camelCase")]
32pub struct PmiLabel {
33 pub id: String,
34 #[serde(rename = "type")]
35 pub kind: String,
36 pub icon: &'static str,
37 pub text: String,
40 pub status: &'static str,
42 pub message: String,
43 pub color: [f32; 3],
44 pub world: [f64; 3],
46 pub enabled: bool,
47 pub open: bool,
49 pub selected: bool,
51 pub text_size_pt: f64,
52}
53
54impl EngineState {
55 pub fn refresh_pmi_overlay(&mut self) {
58 let Some(active) = self.pmi_active_view.clone() else {
59 self.clear_pmi_overlay();
60 return;
61 };
62 if self.any_sketch_mode() {
63 self.clear_pmi_overlay();
64 return;
65 }
66 let camera = self.snapshot_camera();
70 let ids: Vec<String> = self.pmi_report.as_ref().and_then(|r| r.view(&active))
71 .map(|v| v.annotations.iter().filter(|row| matches!(&row.geometry,
72 brep_kernel::PmiGeometry::Leader { anchor: Some(a), balloon: true, .. }
73 if matches!(a.mode, brep_kernel::BalloonAnchorMode::Surface) && a.camera.as_ref() != Some(&camera)))
74 .map(|row| row.id.clone()).collect()).unwrap_or_default();
75 let mut missing = Vec::new();
76 for id in &ids {
77 if let Some(names) = self.pmi_balloon_head_now(id) {
78 missing.extend(names);
79 self.pmi_balloon_stale = Some(id.clone());
80 }
81 }
82 if !missing.is_empty() {
83 missing.sort();
84 missing.dedup();
85 self.request_exact_solids(missing.iter().map(String::as_str));
86 for id in &ids { let _ = self.pmi_balloon_head_now(id); }
87 }
88 let Some(view) = self.pmi_report.as_ref().and_then(|report| report.view(&active)) else {
91 self.clear_pmi_overlay();
92 return;
93 };
94 let text_size = self
95 .pmi_state_shared()
96 .find_view(&active)
97 .map(|view| view.display.text_size_pt)
98 .unwrap_or(12.0);
99 let wpp = self.camera.world_per_pixel();
100 let (_, up, view_dir) = self.camera.basis();
101 let style = PmiLayoutStyle {
102 arrow: ARROW_PX * wpp,
103 text_height: TEXT_PX_AT_12PT * wpp * (text_size / 12.0),
104 view_dir,
105 view_up: up,
106 plane: None,
107 };
108 let mut lines: Vec<f32> = Vec::new();
109 let mut line_colors: Vec<f32> = Vec::new();
110 let mut tris: Vec<f32> = Vec::new();
111 let mut tri_colors: Vec<f32> = Vec::new();
112 let mut explode_step = 0;
113 let playback = self.explode_playback_poses();
115 for row in &view.annotations {
116 if !row.enabled || row.status != PmiStatus::Ok || self.balloon_target_hidden_live(row) {
117 continue;
118 }
119 let geometry = if matches!(row.geometry, brep_kernel::PmiGeometry::Explode { .. }) {
121 explode_step += 1;
122 super::exploded_ops::explode_at(&row.geometry, self.explode_fraction(explode_step - 1))
123 } else {
124 playback.as_ref().and_then(|poses| poses.shown_geometry(row)).unwrap_or_else(|| row.geometry.clone())
125 };
126 let label = playback.as_ref().map_or(row.label_world, |poses| poses.shown(row, row.label_world));
127 let row_style = PmiLayoutStyle {
129 plane: row.plane,
130 ..style
131 };
132 let drawn = pmi_present(&geometry, label, &row.text, &row_style);
133 for polyline in &drawn.polylines {
134 for pair in polyline.windows(2) {
135 for point in pair {
136 lines.extend(point.iter().map(|c| *c as f32));
137 line_colors.extend_from_slice(&PMI_RGB);
138 }
139 }
140 }
141 for arrow in &drawn.arrows {
142 for point in arrow {
143 tris.extend(point.iter().map(|c| *c as f32));
144 tri_colors.extend_from_slice(&PMI_RGB);
145 }
146 for point in [arrow[0], arrow[2], arrow[1]] {
148 tris.extend(point.iter().map(|c| *c as f32));
149 tri_colors.extend_from_slice(&PMI_RGB);
150 }
151 }
152 }
153 let group = if lines.is_empty() && tris.is_empty() {
154 serde_json::json!({ "name": PMI_OVERLAY_GROUP })
155 } else {
156 serde_json::json!({
157 "name": PMI_OVERLAY_GROUP,
158 "renderOrder": 10001,
159 "lines": { "positions": lines, "colors": line_colors },
160 "tris": { "positions": tris, "colors": tri_colors },
161 })
162 };
163 let _ = self.set_overlay_json(&serde_json::json!({ "groups": [group] }).to_string());
164 self.pmi_overlay_key = Some((if wpp > 0.0 { wpp } else { f64::MIN_POSITIVE }, view_dir));
165 self.dirty = true;
166 }
167
168 pub(crate) fn balloon_target_hidden_live(&self, row: &brep_kernel::PmiAnnotationReport) -> bool {
174 match &row.geometry {
175 brep_kernel::PmiGeometry::Leader { balloon: true, anchor: Some(anchor), .. } => {
176 !anchor.solids.is_empty()
177 && anchor
178 .solids
179 .iter()
180 .all(|name| self.scene.solid(name).is_none_or(|solid| !solid.visible))
181 }
182 _ => false,
183 }
184 }
185
186 fn clear_pmi_overlay(&mut self) {
187 if self.pmi_overlay_key.take().is_some() {
188 let _ = self.set_overlay_json(
189 &serde_json::json!({ "groups": [ { "name": PMI_OVERLAY_GROUP } ] }).to_string(),
190 );
191 self.dirty = true;
192 }
193 }
194
195 pub fn ensure_pmi_overlay_current(&mut self) {
199 let Some((baked_wpp, baked_dir)) = self.pmi_overlay_key else {
200 if self.pmi_active_view.is_some() {
201 self.refresh_pmi_overlay();
202 }
203 return;
204 };
205 if self.any_sketch_mode() {
206 self.clear_pmi_overlay();
207 return;
208 }
209 let wpp = self.camera.world_per_pixel();
210 let (_, _, view_dir) = self.camera.basis();
211 let dot = baked_dir[0] * view_dir[0] + baked_dir[1] * view_dir[1] + baked_dir[2] * view_dir[2];
212 let camera = self.snapshot_camera();
213 let balloon_camera_changed = self.pmi_report.as_ref()
214 .and_then(|r| self.pmi_active_view.as_ref().and_then(|id| r.view(id)))
215 .is_some_and(|v| v.annotations.iter().any(|row| matches!(&row.geometry,
216 brep_kernel::PmiGeometry::Leader { anchor: Some(a), balloon: true, .. }
217 if matches!(a.mode, brep_kernel::BalloonAnchorMode::Surface) && a.camera.as_ref() != Some(&camera))));
218 if balloon_camera_changed || super::overlay_wpp_stale(baked_wpp, wpp) || dot < (0.5f64).to_radians().cos() {
219 self.refresh_pmi_overlay();
220 }
221 }
222
223 pub fn pmi_labels_json(&self) -> String {
228 serde_json::to_string(&self.pmi_labels()).unwrap_or_else(|_| "[]".into())
229 }
230
231 pub fn pmi_labels(&self) -> Vec<PmiLabel> {
235 let Some(active) = self.pmi_active_view.as_deref() else {
236 return Vec::new();
237 };
238 let Some(view) = self.pmi_report.as_ref().and_then(|report| report.view(active)) else {
239 return Vec::new();
240 };
241 let explode_chips = matches!(self.settings.workbench.as_str(), "pmi" | "all");
245 if !explode_chips && view.annotations.iter().all(|row| row.kind == "explode") {
246 return Vec::new();
247 }
248 let text_size = self
249 .pmi_state_shared()
250 .find_view(active)
251 .map(|view| view.display.text_size_pt)
252 .unwrap_or(12.0);
253 let playback = self.explode_playback_poses();
255 view.annotations
260 .iter()
261 .filter(|row| row.kind != "explode")
262 .map(|row| {
263 let (status, message) = if row.status == PmiStatus::Ok && self.balloon_target_hidden_live(row) {
268 (PmiStatus::Error, LIVE_HIDDEN_MESSAGE.to_string())
269 } else {
270 (row.status, row.message.clone())
271 };
272 let color = if status == PmiStatus::Error {
273 ERROR_RGB
274 } else if !row.enabled {
275 DISABLED_RGB
276 } else {
277 PMI_RGB
278 };
279 let icon = pmi_type(&row.kind).map(|def| def.icon).unwrap_or("");
280 let text = if status == PmiStatus::Error || row.text.is_empty() {
281 format!("{} {}", icon, row.id)
282 } else {
283 row.text.clone()
284 };
285 PmiLabel {
286 id: row.id.clone(),
287 kind: row.kind.clone(),
288 icon,
289 text,
290 status: match status { PmiStatus::Ok => "ok", PmiStatus::Error => "error" },
291 message,
292 color,
293 world: playback.as_ref().map_or(row.label_world, |poses| poses.shown(row, row.label_world)),
294 enabled: row.enabled,
295 open: self.pmi_open_annotation.as_deref() == Some(row.id.as_str()),
296 selected: self.pmi_selected_annotation.as_deref() == Some(row.id.as_str()),
297 text_size_pt: text_size,
298 }
299 })
300 .collect()
301 }
302}