1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
//! For drawing text on over 3D graphics, using EGUI's painer.
use egui::{Align2, Color32, FontFamily, FontId, Pos2};
use lin_alg::f32::Vec3;
use crate::{UiSettings, graphics::GraphicsState, gui::GuiState, viewport_rect};
#[derive(Debug, Clone)]
pub struct TextOverlay {
pub text: String,
pub size: f32,
/// Red, Greed, Blue, alpha
pub color: (u8, u8, u8, u8),
pub font_family: FontFamily,
}
impl Default for TextOverlay {
fn default() -> Self {
Self {
text: String::new(),
size: 13.,
color: (255, 255, 255, 255),
font_family: FontFamily::Proportional,
}
}
}
pub(crate) fn draw_text_overlay(
graphics_state: &GraphicsState,
gui: &GuiState,
ui_settings: &UiSettings,
// These are in physical pixels.
width: u32,
height: u32,
) {
let ctx = gui.egui_state.egui_ctx();
// Compute label positions in screen space
let labels = graphics_state.collect_entity_labels(
width,
height,
ui_settings,
gui.size,
ctx.pixels_per_point(),
);
// Paint in the foreground layer
let painter = ctx.layer_painter(egui::LayerId::new(
egui::Order::Foreground,
egui::Id::new("entity_labels"),
));
for (pos, overlay) in labels {
let (r, g, b, a) = overlay.color;
painter.text(
pos,
Align2::CENTER_BOTTOM,
&overlay.text,
// todo: Font size may need to be part of the label.
FontId::new(overlay.size, overlay.font_family.clone()),
Color32::from_rgba_unmultiplied(r, g, b, a),
);
}
}
impl GraphicsState {
/// We use this for the text overlay.
/// Project a world-space point to screen-space (in egui points).
/// Returns None if behind camera or outside clip space.
pub fn world_to_screen(
&self,
world: Vec3,
// In physical pixels
width: u32,
height: u32,
ui_settings: &UiSettings,
ui_size: (f32, f32),
pixels_per_pt: f32,
) -> Option<Pos2> {
// Convert physical pixels to logical pixels (egui points) so the result is
// in egui point space and Pos2 is placed correctly on HiDPI displays.
let logical_width = (width as f32 / pixels_per_pt).round() as u32;
let logical_height = (height as f32 / pixels_per_pt).round() as u32;
let (x, y, eff_width, eff_height) = viewport_rect(
ui_size,
logical_width,
logical_height,
ui_settings,
pixels_per_pt,
);
let (in_view, ndc) = self.scene.camera.in_view(world);
if !in_view {
return None;
}
// NDC -> pixels in 3D viewport
let sx = x + (ndc.0 * 0.5 + 0.5) * eff_width;
let sy = y + (1.0 - (ndc.1 * 0.5 + 0.5)) * eff_height; // flip Y for top-left origin
Some(Pos2::new(sx, sy))
}
/// Convenience: gather label screen positions for all entities that have `overlay_text`.
pub fn collect_entity_labels(
&self,
// Physical pixels
width: u32,
height: u32,
ui_settings: &UiSettings,
ui_size: (f32, f32),
pixels_per_pt: f32,
) -> Vec<(Pos2, &TextOverlay)> {
let mut out = Vec::new();
for e in &self.scene.entities {
if let Some(overlay) = &e.overlay_text {
// Slight vertical offset above the entity (tune as you like).
let label_world = Vec3 {
x: e.position.x,
y: e.position.y + 0.05 * e.scale, // small lift
z: e.position.z,
};
if let Some(p) = self.world_to_screen(
label_world,
width,
height,
ui_settings,
ui_size,
pixels_per_pt,
) {
out.push((p, overlay));
}
}
}
out
}
}