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 EXPLODE_ARROW_RGB: [f32; 3] = [0.0, 0.0, 0.0];
23const EXPLODE_ARROW_ON_DARK_RGB: [f32; 3] = [0.92, 0.94, 0.96];
24const INSERTION_HEAD_PX: f64 = 20.0;
27const ERROR_RGB: [f32; 3] = [0.97, 0.32, 0.29];
29const DISABLED_RGB: [f32; 3] = [0.55, 0.55, 0.55];
31pub(crate) const LIVE_HIDDEN_MESSAGE: &str = "target hidden in the viewport: show it, or update the view's visibility";
34
35#[derive(Debug, Clone, PartialEq, serde::Serialize)]
38#[serde(rename_all = "camelCase")]
39pub struct PmiLabel {
40 pub id: String,
41 #[serde(rename = "type")]
42 pub kind: String,
43 pub icon: &'static str,
44 pub text: String,
47 pub status: &'static str,
49 pub message: String,
50 pub color: [f32; 3],
51 pub world: [f64; 3],
53 pub enabled: bool,
54 pub open: bool,
56 pub selected: bool,
58 pub text_size_pt: f64,
59}
60
61impl EngineState {
62 pub fn refresh_pmi_overlay(&mut self) {
65 let Some(active) = self.pmi_active_view.clone() else {
66 self.clear_pmi_overlay();
67 return;
68 };
69 if self.any_sketch_mode() {
70 self.clear_pmi_overlay();
71 return;
72 }
73 let camera = self.snapshot_camera();
77 let ids: Vec<String> = self.pmi_report.as_ref().and_then(|r| r.view(&active))
78 .map(|v| v.annotations.iter().filter(|row| matches!(&row.geometry,
79 brep_kernel::PmiGeometry::Leader { anchor: Some(a), balloon: true, .. }
80 if matches!(a.mode, brep_kernel::BalloonAnchorMode::Surface) && a.camera.as_ref() != Some(&camera)))
81 .map(|row| row.id.clone()).collect()).unwrap_or_default();
82 let mut missing = Vec::new();
83 for id in &ids {
84 if let Some(names) = self.pmi_balloon_head_now(id) {
85 missing.extend(names);
86 self.pmi_balloon_stale = Some(id.clone());
87 }
88 }
89 if !missing.is_empty() {
90 missing.sort();
91 missing.dedup();
92 self.request_exact_solids(missing.iter().map(String::as_str));
93 for id in &ids { let _ = self.pmi_balloon_head_now(id); }
94 }
95 let Some(view) = self.pmi_report.as_ref().and_then(|report| report.view(&active)) else {
98 self.clear_pmi_overlay();
99 return;
100 };
101 let text_size = self
102 .pmi_state_shared()
103 .find_view(&active)
104 .map(|view| view.display.text_size_pt)
105 .unwrap_or(12.0);
106 let wpp = self.camera.world_per_pixel();
107 let (_, up, view_dir) = self.camera.basis();
108 let style = PmiLayoutStyle {
109 arrow: ARROW_PX * wpp,
110 text_height: TEXT_PX_AT_12PT * wpp * (text_size / 12.0),
111 view_dir,
112 view_up: up,
113 plane: None,
114 };
115 let mut lines: Vec<f32> = Vec::new();
116 let mut line_colors: Vec<f32> = Vec::new();
117 let mut tris: Vec<f32> = Vec::new();
118 let mut tri_colors: Vec<f32> = Vec::new();
119 let mut explode_step = 0;
120 let mut arrows: Vec<([f64; 3], [f64; 3])> = Vec::new();
121 let mut center_marks: Vec<(String, Vec<[[f64; 3]; 2]>)> = Vec::new();
122 let playback = self.explode_playback_poses();
124 for row in &view.annotations {
125 if !row.enabled || row.status != PmiStatus::Ok || self.balloon_target_hidden_live(row) {
126 continue;
127 }
128 let geometry = if matches!(row.geometry, brep_kernel::PmiGeometry::Explode { .. }) {
130 explode_step += 1;
131 super::exploded_ops::explode_at(&row.geometry, self.explode_fraction(explode_step - 1))
132 } else {
133 playback.as_ref().and_then(|poses| poses.shown_geometry(row)).unwrap_or_else(|| row.geometry.clone())
134 };
135 let label = playback.as_ref().map_or(row.label_world, |poses| poses.shown(row, row.label_world));
136 let row_style = PmiLayoutStyle {
138 plane: row.plane,
139 ..style
140 };
141 if let brep_kernel::PmiGeometry::Explode { solids, translate, trace, .. } = &geometry {
144 if *trace {
145 let mut now = crate::camera::Aabb::empty();
146 for solid in self.scene.solids() {
147 if solids.iter().any(|s| solid.name == *s || solid.name.strip_prefix(s.as_str()).is_some_and(|rest| rest.starts_with(':'))) {
148 now.union(&solid.bbox);
149 }
150 }
151 if !now.is_empty() {
152 let home = crate::camera::Aabb {
153 min: std::array::from_fn(|k| now.min[k] - translate[k]),
154 max: std::array::from_fn(|k| now.max[k] - translate[k]),
155 };
156 arrows.extend(self.outline_arrow(&home, *translate));
157 }
158 }
159 continue;
160 }
161 let drawn = pmi_present(&geometry, label, &row.text, &row_style);
162 if let brep_kernel::PmiGeometry::CenterMark { marks, .. } = &geometry {
163 let arm = |line: &Vec<[f64; 3]>| -> [f64; 3] {
166 let d: [f64; 3] = std::array::from_fn(|k| line[1][k] - line[0][k]);
167 let length = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
168 if length > 0.0 { d.map(|c| c / length) } else { [0.0; 3] }
169 };
170 let arms = (0..marks.len())
171 .filter_map(|k| Some([arm(drawn.polylines.get(6 * k)?), arm(drawn.polylines.get(6 * k + 1)?)]))
172 .collect();
173 center_marks.push((row.id.clone(), arms));
174 }
175 let rgb = PMI_RGB;
176 for polyline in &drawn.polylines {
177 for pair in polyline.windows(2) {
178 for point in pair {
179 lines.extend(point.iter().map(|c| *c as f32));
180 line_colors.extend_from_slice(&rgb);
181 }
182 }
183 }
184 for arrow in &drawn.arrows {
185 for point in arrow {
186 tris.extend(point.iter().map(|c| *c as f32));
187 tri_colors.extend_from_slice(&rgb);
188 }
189 for point in [arrow[0], arrow[2], arrow[1]] {
191 tris.extend(point.iter().map(|c| *c as f32));
192 tri_colors.extend_from_slice(&rgb);
193 }
194 }
195 }
196 arrows.extend(self.instructions_insertion_arrows());
202 let ground = if self.scene.line_art.is_some() { crate::thumbnail::line_art::BACKGROUND } else { self.settings.background };
203 let light = 0.2126 * ground[0] + 0.7152 * ground[1] + 0.0722 * ground[2] > 0.5;
204 let ink = if light { EXPLODE_ARROW_RGB } else { EXPLODE_ARROW_ON_DARK_RGB };
205 for (tail, tip) in arrows {
206 let d = [tip[0] - tail[0], tip[1] - tail[1], tip[2] - tail[2]];
207 let length = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
208 if !(length > 1e-9) {
209 continue;
210 }
211 let d = d.map(|v| v / length);
212 let head = (INSERTION_HEAD_PX * wpp).min(length * 0.6);
213 let w = crate::geometry3d::cross3(view_dir, d);
214 let wl = (w[0] * w[0] + w[1] * w[1] + w[2] * w[2]).sqrt();
215 let w = if wl > 1e-9 { w.map(|v| v / wl) } else { up };
216 let base: [f64; 3] = std::array::from_fn(|k| tip[k] - d[k] * head);
217 for point in [tail, base] {
218 lines.extend(point.iter().map(|c| *c as f32));
219 line_colors.extend_from_slice(&ink);
220 }
221 let half = head * 0.35;
222 let corners = [tip, std::array::from_fn(|k| base[k] + w[k] * half), std::array::from_fn(|k| base[k] - w[k] * half)];
223 for point in [corners[0], corners[1], corners[2], corners[0], corners[2], corners[1]] {
224 tris.extend(point.iter().map(|c| *c as f32));
225 tri_colors.extend_from_slice(&ink);
226 }
227 }
228 let group = if lines.is_empty() && tris.is_empty() {
229 serde_json::json!({ "name": PMI_OVERLAY_GROUP })
230 } else {
231 serde_json::json!({
232 "name": PMI_OVERLAY_GROUP,
233 "renderOrder": 10001,
234 "lines": { "positions": lines, "colors": line_colors },
235 "tris": { "positions": tris, "colors": tri_colors },
236 })
237 };
238 let _ = self.set_overlay_json(&serde_json::json!({ "groups": [group] }).to_string());
239 self.pmi_overlay_key = Some((if wpp > 0.0 { wpp } else { f64::MIN_POSITIVE }, view_dir));
240 self.pmi_center_marks_drawn = (center_marks, view_dir);
241 self.dirty = true;
242 }
243
244 pub fn pmi_center_marks_json(&self) -> String {
250 let (marks, view_dir) = &self.pmi_center_marks_drawn;
251 serde_json::json!({
252 "viewDir": view_dir,
253 "marks": marks.iter().map(|(id, arms)| serde_json::json!({ "id": id, "arms": arms })).collect::<Vec<_>>(),
254 })
255 .to_string()
256 }
257
258 pub(crate) fn balloon_target_hidden_live(&self, row: &brep_kernel::PmiAnnotationReport) -> bool {
264 match &row.geometry {
265 brep_kernel::PmiGeometry::Leader { balloon: true, anchor: Some(anchor), .. } => {
266 !anchor.solids.is_empty()
267 && anchor
268 .solids
269 .iter()
270 .all(|name| self.scene.solid(name).is_none_or(|solid| !solid.visible))
271 }
272 _ => false,
273 }
274 }
275
276 fn clear_pmi_overlay(&mut self) {
277 self.pmi_center_marks_drawn.0.clear();
278 if self.pmi_overlay_key.take().is_some() {
279 let _ = self.set_overlay_json(
280 &serde_json::json!({ "groups": [ { "name": PMI_OVERLAY_GROUP } ] }).to_string(),
281 );
282 self.dirty = true;
283 }
284 }
285
286 pub fn ensure_pmi_overlay_current(&mut self) {
290 let Some((baked_wpp, baked_dir)) = self.pmi_overlay_key else {
291 if self.pmi_active_view.is_some() {
292 self.refresh_pmi_overlay();
293 }
294 return;
295 };
296 if self.any_sketch_mode() {
297 self.clear_pmi_overlay();
298 return;
299 }
300 let wpp = self.camera.world_per_pixel();
301 let (_, _, view_dir) = self.camera.basis();
302 let dot = baked_dir[0] * view_dir[0] + baked_dir[1] * view_dir[1] + baked_dir[2] * view_dir[2];
303 let camera = self.snapshot_camera();
304 let balloon_camera_changed = self.pmi_report.as_ref()
305 .and_then(|r| self.pmi_active_view.as_ref().and_then(|id| r.view(id)))
306 .is_some_and(|v| v.annotations.iter().any(|row| matches!(&row.geometry,
307 brep_kernel::PmiGeometry::Leader { anchor: Some(a), balloon: true, .. }
308 if matches!(a.mode, brep_kernel::BalloonAnchorMode::Surface) && a.camera.as_ref() != Some(&camera))));
309 if balloon_camera_changed || super::overlay_wpp_stale(baked_wpp, wpp) || dot < (0.5f64).to_radians().cos() {
310 self.refresh_pmi_overlay();
311 }
312 }
313
314 pub fn pmi_labels_json(&self) -> String {
319 serde_json::to_string(&self.pmi_labels()).unwrap_or_else(|_| "[]".into())
320 }
321
322 pub fn pmi_labels(&self) -> Vec<PmiLabel> {
326 let Some(active) = self.pmi_active_view.as_deref() else {
327 return Vec::new();
328 };
329 let Some(view) = self.pmi_report.as_ref().and_then(|report| report.view(active)) else {
330 return Vec::new();
331 };
332 let explode_chips = matches!(self.settings.workbench.as_str(), "pmi" | "all");
336 if !explode_chips && view.annotations.iter().all(|row| matches!(row.kind.as_str(), "explode" | "hide")) {
337 return Vec::new();
338 }
339 let text_size = self
340 .pmi_state_shared()
341 .find_view(active)
342 .map(|view| view.display.text_size_pt)
343 .unwrap_or(12.0);
344 let playback = self.explode_playback_poses();
346 view.annotations
356 .iter()
357 .filter(|row| !matches!(row.kind.as_str(), "explode" | "hide"))
358 .filter(|row| !(row.kind == "centerline" && row.status == PmiStatus::Ok))
359 .map(|row| {
360 let (status, message) = if row.status == PmiStatus::Ok && self.balloon_target_hidden_live(row) {
365 (PmiStatus::Error, LIVE_HIDDEN_MESSAGE.to_string())
366 } else {
367 (row.status, row.message.clone())
368 };
369 let color = if status == PmiStatus::Error {
370 ERROR_RGB
371 } else if !row.enabled {
372 DISABLED_RGB
373 } else {
374 PMI_RGB
375 };
376 let icon = pmi_type(&row.kind).map(|def| def.icon).unwrap_or("");
377 let text = if status == PmiStatus::Error || row.text.is_empty() {
378 format!("{} {}", icon, row.id)
379 } else {
380 row.text.clone()
381 };
382 PmiLabel {
383 id: row.id.clone(),
384 kind: row.kind.clone(),
385 icon,
386 text,
387 status: match status { PmiStatus::Ok => "ok", PmiStatus::Error => "error" },
388 message,
389 color,
390 world: playback.as_ref().map_or(row.label_world, |poses| poses.shown(row, row.label_world)),
391 enabled: row.enabled,
392 open: self.pmi_open_annotation.as_deref() == Some(row.id.as_str()),
393 selected: self.pmi_selected_annotation.as_deref() == Some(row.id.as_str()),
394 text_size_pt: text_size,
395 }
396 })
397 .collect()
398 }
399}