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
28impl EngineState {
29 pub fn refresh_pmi_overlay(&mut self) {
32 let Some(active) = self.pmi_active_view.clone() else {
33 self.clear_pmi_overlay();
34 return;
35 };
36 if self.sketch_mode() {
37 self.clear_pmi_overlay();
38 return;
39 }
40 let Some(view) = self.pmi_report.as_ref().and_then(|report| report.view(&active)).cloned() else {
41 self.clear_pmi_overlay();
42 return;
43 };
44 let text_size = self
45 .pmi_state()
46 .find_view(&active)
47 .map(|view| view.display.text_size_pt)
48 .unwrap_or(12.0);
49 let wpp = self.camera.world_per_pixel();
50 let (_, up, view_dir) = self.camera.basis();
51 let style = PmiLayoutStyle {
52 arrow: ARROW_PX * wpp,
53 text_height: TEXT_PX_AT_12PT * wpp * (text_size / 12.0),
54 view_dir,
55 view_up: up,
56 plane: None,
57 };
58 let mut lines: Vec<f32> = Vec::new();
59 let mut line_colors: Vec<f32> = Vec::new();
60 let mut tris: Vec<f32> = Vec::new();
61 let mut tri_colors: Vec<f32> = Vec::new();
62 for row in &view.annotations {
63 if !row.enabled || row.status != PmiStatus::Ok || self.balloon_target_hidden_live(row) {
64 continue;
65 }
66 let row_style = PmiLayoutStyle {
68 plane: row.plane,
69 ..style
70 };
71 let drawn = pmi_present(&row.geometry, row.label_world, &row.text, &row_style);
72 for polyline in &drawn.polylines {
73 for pair in polyline.windows(2) {
74 for point in pair {
75 lines.extend(point.iter().map(|c| *c as f32));
76 line_colors.extend_from_slice(&PMI_RGB);
77 }
78 }
79 }
80 for arrow in &drawn.arrows {
81 for point in arrow {
82 tris.extend(point.iter().map(|c| *c as f32));
83 tri_colors.extend_from_slice(&PMI_RGB);
84 }
85 for point in [arrow[0], arrow[2], arrow[1]] {
87 tris.extend(point.iter().map(|c| *c as f32));
88 tri_colors.extend_from_slice(&PMI_RGB);
89 }
90 }
91 }
92 let group = if lines.is_empty() && tris.is_empty() {
93 serde_json::json!({ "name": PMI_OVERLAY_GROUP })
94 } else {
95 serde_json::json!({
96 "name": PMI_OVERLAY_GROUP,
97 "renderOrder": 10001,
98 "lines": { "positions": lines, "colors": line_colors },
99 "tris": { "positions": tris, "colors": tri_colors },
100 })
101 };
102 let _ = self.set_overlay_json(&serde_json::json!({ "groups": [group] }).to_string());
103 self.pmi_overlay_key = Some((if wpp > 0.0 { wpp } else { f64::MIN_POSITIVE }, view_dir));
104 self.dirty = true;
105 }
106
107 pub(crate) fn balloon_target_hidden_live(&self, row: &brep_kernel::PmiAnnotationReport) -> bool {
113 match &row.geometry {
114 brep_kernel::PmiGeometry::Leader { balloon: true, anchor: Some(anchor), .. } => {
115 !anchor.solids.is_empty()
116 && anchor
117 .solids
118 .iter()
119 .all(|name| self.scene.solid(name).is_none_or(|solid| !solid.visible))
120 }
121 _ => false,
122 }
123 }
124
125 fn clear_pmi_overlay(&mut self) {
126 if self.pmi_overlay_key.take().is_some() {
127 let _ = self.set_overlay_json(
128 &serde_json::json!({ "groups": [ { "name": PMI_OVERLAY_GROUP } ] }).to_string(),
129 );
130 self.dirty = true;
131 }
132 }
133
134 pub fn ensure_pmi_overlay_current(&mut self) {
138 let Some((baked_wpp, baked_dir)) = self.pmi_overlay_key else {
139 if self.pmi_active_view.is_some() {
140 self.refresh_pmi_overlay();
141 }
142 return;
143 };
144 if self.sketch_mode() {
145 self.clear_pmi_overlay();
146 return;
147 }
148 let wpp = self.camera.world_per_pixel();
149 let (_, _, view_dir) = self.camera.basis();
150 let dot = baked_dir[0] * view_dir[0] + baked_dir[1] * view_dir[1] + baked_dir[2] * view_dir[2];
151 if super::overlay_wpp_stale(baked_wpp, wpp) || dot < (0.5f64).to_radians().cos() {
152 self.refresh_pmi_overlay();
153 }
154 }
155
156 pub fn pmi_labels_json(&self) -> String {
161 let Some(active) = self.pmi_active_view.as_deref() else {
162 return "[]".into();
163 };
164 let Some(view) = self.pmi_report.as_ref().and_then(|report| report.view(active)) else {
165 return "[]".into();
166 };
167 let text_size = self
168 .pmi_state()
169 .find_view(active)
170 .map(|view| view.display.text_size_pt)
171 .unwrap_or(12.0);
172 let rows: Vec<serde_json::Value> = view
173 .annotations
174 .iter()
175 .map(|row| {
176 let (status, message) = if row.status == PmiStatus::Ok && self.balloon_target_hidden_live(row) {
181 (PmiStatus::Error, LIVE_HIDDEN_MESSAGE.to_string())
182 } else {
183 (row.status, row.message.clone())
184 };
185 let color = if status == PmiStatus::Error {
186 ERROR_RGB
187 } else if !row.enabled {
188 DISABLED_RGB
189 } else {
190 PMI_RGB
191 };
192 let icon = pmi_type(&row.kind).map(|def| def.icon).unwrap_or("");
193 let text = if status == PmiStatus::Error {
194 format!("{} {}", icon, row.id)
195 } else if row.text.is_empty() {
196 format!("{} {}", icon, row.id)
197 } else {
198 row.text.clone()
199 };
200 serde_json::json!({
201 "id": row.id,
202 "type": row.kind,
203 "icon": icon,
204 "text": text,
205 "status": match status { PmiStatus::Ok => "ok", PmiStatus::Error => "error" },
206 "message": message,
207 "color": [color[0], color[1], color[2]],
208 "world": row.label_world,
209 "enabled": row.enabled,
210 "open": self.pmi_open_annotation.as_deref() == Some(row.id.as_str()),
211 "selected": self.pmi_selected_annotation.as_deref() == Some(row.id.as_str()),
212 "textSizePt": text_size,
213 })
214 })
215 .collect();
216 serde_json::Value::Array(rows).to_string()
217 }
218}