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";
12const ARROW_PX: f64 = 12.0;
14const 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];
24
25impl EngineState {
26 pub fn refresh_pmi_overlay(&mut self) {
29 let Some(active) = self.pmi_active_view.clone() else {
30 self.clear_pmi_overlay();
31 return;
32 };
33 if self.sketch_mode() {
34 self.clear_pmi_overlay();
35 return;
36 }
37 let Some(view) = self.pmi_report.as_ref().and_then(|report| report.view(&active)).cloned() else {
38 self.clear_pmi_overlay();
39 return;
40 };
41 let text_size = self
42 .pmi_state()
43 .find_view(&active)
44 .map(|view| view.display.text_size_pt)
45 .unwrap_or(12.0);
46 let wpp = self.camera.world_per_pixel();
47 let (_, up, view_dir) = self.camera.basis();
48 let style = PmiLayoutStyle {
49 arrow: ARROW_PX * wpp,
50 text_height: TEXT_PX_AT_12PT * wpp * (text_size / 12.0),
51 view_dir,
52 view_up: up,
53 plane: None,
54 };
55 let mut lines: Vec<f32> = Vec::new();
56 let mut line_colors: Vec<f32> = Vec::new();
57 let mut tris: Vec<f32> = Vec::new();
58 let mut tri_colors: Vec<f32> = Vec::new();
59 for row in &view.annotations {
60 if !row.enabled || row.status != PmiStatus::Ok {
61 continue;
62 }
63 let row_style = PmiLayoutStyle {
65 plane: row.plane,
66 ..style
67 };
68 let drawn = pmi_present(&row.geometry, row.label_world, &row.text, &row_style);
69 for polyline in &drawn.polylines {
70 for pair in polyline.windows(2) {
71 for point in pair {
72 lines.extend(point.iter().map(|c| *c as f32));
73 line_colors.extend_from_slice(&PMI_RGB);
74 }
75 }
76 }
77 for arrow in &drawn.arrows {
78 for point in arrow {
79 tris.extend(point.iter().map(|c| *c as f32));
80 tri_colors.extend_from_slice(&PMI_RGB);
81 }
82 for point in [arrow[0], arrow[2], arrow[1]] {
84 tris.extend(point.iter().map(|c| *c as f32));
85 tri_colors.extend_from_slice(&PMI_RGB);
86 }
87 }
88 }
89 let group = if lines.is_empty() && tris.is_empty() {
90 serde_json::json!({ "name": PMI_OVERLAY_GROUP })
91 } else {
92 serde_json::json!({
93 "name": PMI_OVERLAY_GROUP,
94 "renderOrder": 10001,
95 "lines": { "positions": lines, "colors": line_colors },
96 "tris": { "positions": tris, "colors": tri_colors },
97 })
98 };
99 let _ = self.set_overlay_json(&serde_json::json!({ "groups": [group] }).to_string());
100 self.pmi_overlay_key = Some((if wpp > 0.0 { wpp } else { f64::MIN_POSITIVE }, view_dir));
101 self.dirty = true;
102 }
103
104 fn clear_pmi_overlay(&mut self) {
105 if self.pmi_overlay_key.take().is_some() {
106 let _ = self.set_overlay_json(
107 &serde_json::json!({ "groups": [ { "name": PMI_OVERLAY_GROUP } ] }).to_string(),
108 );
109 self.dirty = true;
110 }
111 }
112
113 pub fn ensure_pmi_overlay_current(&mut self) {
117 let Some((baked_wpp, baked_dir)) = self.pmi_overlay_key else {
118 if self.pmi_active_view.is_some() {
119 self.refresh_pmi_overlay();
120 }
121 return;
122 };
123 if self.sketch_mode() {
124 self.clear_pmi_overlay();
125 return;
126 }
127 let wpp = self.camera.world_per_pixel();
128 let (_, _, view_dir) = self.camera.basis();
129 let dot = baked_dir[0] * view_dir[0] + baked_dir[1] * view_dir[1] + baked_dir[2] * view_dir[2];
130 if super::overlay_wpp_stale(baked_wpp, wpp) || dot < (0.5f64).to_radians().cos() {
131 self.refresh_pmi_overlay();
132 }
133 }
134
135 pub fn pmi_labels_json(&self) -> String {
140 let Some(active) = self.pmi_active_view.as_deref() else {
141 return "[]".into();
142 };
143 let Some(view) = self.pmi_report.as_ref().and_then(|report| report.view(active)) else {
144 return "[]".into();
145 };
146 let text_size = self
147 .pmi_state()
148 .find_view(active)
149 .map(|view| view.display.text_size_pt)
150 .unwrap_or(12.0);
151 let rows: Vec<serde_json::Value> = view
152 .annotations
153 .iter()
154 .map(|row| {
155 let color = if row.status == PmiStatus::Error {
156 ERROR_RGB
157 } else if !row.enabled {
158 DISABLED_RGB
159 } else {
160 PMI_RGB
161 };
162 let icon = pmi_type(&row.kind).map(|def| def.icon).unwrap_or("");
163 let text = if row.status == PmiStatus::Error {
164 format!("{} {}", icon, row.id)
165 } else if row.text.is_empty() {
166 format!("{} {}", icon, row.id)
167 } else {
168 row.text.clone()
169 };
170 serde_json::json!({
171 "id": row.id,
172 "type": row.kind,
173 "icon": icon,
174 "text": text,
175 "status": match row.status { PmiStatus::Ok => "ok", PmiStatus::Error => "error" },
176 "message": row.message,
177 "color": [color[0], color[1], color[2]],
178 "world": row.label_world,
179 "enabled": row.enabled,
180 "open": self.pmi_open_annotation.as_deref() == Some(row.id.as_str()),
181 "textSizePt": text_size,
182 })
183 })
184 .collect();
185 serde_json::Value::Array(rows).to_string()
186 }
187}