diff --git a/crates/render/src/text.rs b/crates/render/src/text.rs index b37c8c6..dce01ee 100644 --- a/crates/render/src/text.rs +++ b/crates/render/src/text.rs @@ -30,7 +30,7 @@ use font8x8::UnicodeFonts; /// Строит атлас глифов ASCII 32..127 (R8, альфа). pub fn build_atlas() -> Vec { let mut atlas = vec![0u8; (ATLAS_W * ATLAS_H) as usize]; - for code in 32u8..127 { + for code in 32u8..=127 { let glyph = font8x8::BASIC_FONTS .get(char::from(code)) .unwrap_or([0u8; 8]); @@ -47,14 +47,39 @@ pub fn build_atlas() -> Vec { } } } + // 127 — служебный «белый квадрат»: им заливаются прямоугольники (фон + // меню/затемнение), у шрифта такого символа нет. + let index = 127u32 - 32; + let gx = (index % 16) * 8; + let gy = (index / 16) * 8; + for row in 0..8u32 { + for col in 0..8u32 { + atlas[((gy + row) * ATLAS_W + gx + col) as usize] = 255; + } + } atlas } /// UV-прямоугольник глифа в атласе. pub fn glyph_uv(code: u8) -> ([f32; 2], [f32; 2]) { - let index = code.clamp(32, 126) as u32 - 32; + let index = code.clamp(32, 127) as u32 - 32; let gx = (index % 16) * 8; let gy = (index / 16) * 8; + // Сплошной глиф растягивается на весь экран: берём UV с отступом в + // полтексела, иначе линейная фильтрация подмешивает соседний глиф и на + // границе заливки появляется полоса. + if code == 127 { + return ( + [ + (gx as f32 + 0.5) / ATLAS_W as f32, + (gy as f32 + 0.5) / ATLAS_H as f32, + ], + [ + (gx as f32 + 7.5) / ATLAS_W as f32, + (gy as f32 + 7.5) / ATLAS_H as f32, + ], + ); + } ( [gx as f32 / ATLAS_W as f32, gy as f32 / ATLAS_H as f32], [ @@ -110,6 +135,22 @@ mod tests { assert!(lit > 100, "атлас пуст: {lit}"); } + /// Служебный глиф 127 — сплошной белый квадрат для заливки фона. + #[test] + fn atlas_has_solid_glyph() { + let atlas = build_atlas(); + let (uv0, uv1) = glyph_uv(127); + let x0 = (uv0[0] * ATLAS_W as f32) as u32; + let y0 = (uv0[1] * ATLAS_H as f32) as u32; + let x1 = (uv1[0] * ATLAS_W as f32) as u32; + let y1 = (uv1[1] * ATLAS_H as f32) as u32; + for y in y0..y1 { + for x in x0..x1 { + assert_eq!(atlas[(y * ATLAS_W + x) as usize], 255, "пиксель {x},{y}"); + } + } + } + #[test] fn vertices_for_text() { let items = vec![TextItem { diff --git a/crates/render/src/world_renderer.rs b/crates/render/src/world_renderer.rs index 0539037..52d45a5 100644 --- a/crates/render/src/world_renderer.rs +++ b/crates/render/src/world_renderer.rs @@ -1278,7 +1278,7 @@ impl WorldRenderer { resource: screen_buffer.as_entire_binding(), }], }); - let capacity = 16384; + let capacity = 65536; let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("text vertices"), size: (capacity * std::mem::size_of::()) as u64, @@ -1465,6 +1465,25 @@ impl WorldRenderer { self.text.items.clear(); } + /// Заливает прямоугольник цветом (затемнение под меню). + /// + /// Панелей у HUD пока нет, поэтому берём служебный сплошной глиф 127 из + /// атласа и растягиваем его сеткой. Годится только для прямоугольников, + /// залитых одним цветом, — ровно это нужно модальному оверлею. + pub fn push_rect(&mut self, x: f32, y: f32, width: f32, height: f32, color: [f32; 4]) { + if width <= 0.0 || height <= 0.0 { + return; + } + let scale = (height / 8.0).max(1.0); + let step = 8.0 * scale; + let cols = (width / step).ceil().max(1.0) as usize; + let rows = (height / step).ceil().max(1.0) as usize; + let line: String = std::iter::repeat_n('\u{7f}', cols).collect(); + for row in 0..rows { + self.push_text(x, y + row as f32 * step, scale, color, line.clone()); + } + } + /// Загружает оверлей на GPU (размер экрана + вершины). fn upload_text(&self, queue: &wgpu::Queue, width: u32, height: u32) { let screen: [f32; 4] = [width as f32, height as f32, 0.0, 0.0];