1pub mod cross_tile_index;
17#[cfg(feature = "text-shaping")]
18pub mod text_shaper;
19
20use crate::camera_projection::CameraProjection;
21use rustial_math::{GeoCoord, TileId, WorldBounds};
22use std::collections::{BTreeSet, HashMap, HashSet};
23
24#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
26pub struct GlyphKey {
27 pub font_stack: String,
29 pub codepoint: char,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
35pub enum SymbolAnchor {
36 Center,
38 Top,
40 Bottom,
42 Left,
44 Right,
46 TopLeft,
48 TopRight,
50 BottomLeft,
52 BottomRight,
54}
55
56impl SymbolAnchor {
57 fn offset_signs(self) -> [f64; 2] {
58 match self {
59 SymbolAnchor::Center => [0.0, 0.0],
60 SymbolAnchor::Top => [0.0, 1.0],
61 SymbolAnchor::Bottom => [0.0, -1.0],
62 SymbolAnchor::Left => [-1.0, 0.0],
63 SymbolAnchor::Right => [1.0, 0.0],
64 SymbolAnchor::TopLeft => [-1.0, 1.0],
65 SymbolAnchor::TopRight => [1.0, 1.0],
66 SymbolAnchor::BottomLeft => [-1.0, -1.0],
67 SymbolAnchor::BottomRight => [1.0, -1.0],
68 }
69 }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
74pub enum SymbolWritingMode {
75 #[default]
77 Horizontal,
78 Vertical,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
84pub enum SymbolTextJustify {
85 #[default]
87 Auto,
88 Left,
90 Center,
92 Right,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
98pub enum SymbolTextTransform {
99 #[default]
101 None,
102 Uppercase,
104 Lowercase,
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
110pub enum SymbolIconTextFit {
111 #[default]
113 None,
114 Width,
116 Height,
118 Both,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
130pub enum SymbolPlacement {
131 #[default]
133 Point,
134 Line,
136}
137
138#[derive(Debug, Clone, PartialEq)]
140pub struct GlyphRaster {
141 pub width: u16,
143 pub height: u16,
145 pub advance_x: f32,
147 pub bearing_x: i16,
149 pub bearing_y: i16,
151 pub alpha: Vec<u8>,
153}
154
155impl GlyphRaster {
156 pub fn new(
158 width: u16,
159 height: u16,
160 advance_x: f32,
161 bearing_x: i16,
162 bearing_y: i16,
163 alpha: Vec<u8>,
164 ) -> Self {
165 Self {
166 width,
167 height,
168 advance_x,
169 bearing_x,
170 bearing_y,
171 alpha,
172 }
173 }
174}
175
176#[derive(Debug, Clone, PartialEq)]
178pub struct GlyphAtlasEntry {
179 pub key: GlyphKey,
181 pub origin: [u16; 2],
183 pub size: [u16; 2],
185 pub advance_x: f32,
187 pub bearing_x: i16,
189 pub bearing_y: i16,
191}
192
193pub trait GlyphProvider: Send + Sync {
195 fn load_glyph(&self, font_stack: &str, codepoint: char) -> Option<GlyphRaster>;
197
198 fn render_em_pixels(&self) -> f32 {
204 24.0
205 }
206}
207
208#[derive(Debug, Clone, Default)]
210pub struct ProceduralGlyphProvider;
211
212impl ProceduralGlyphProvider {
213 pub fn new() -> Self {
215 Self
216 }
217}
218
219impl GlyphProvider for ProceduralGlyphProvider {
220 fn load_glyph(&self, _font_stack: &str, codepoint: char) -> Option<GlyphRaster> {
221 let width = 8u16;
222 let height = 12u16;
223 let mut alpha = vec![0u8; width as usize * height as usize];
224 let seed = codepoint as u32;
225 for y in 0..height as usize {
226 for x in 0..width as usize {
227 let border =
228 x == 0 || y == 0 || x + 1 == width as usize || y + 1 == height as usize;
229 let bits = ((seed >> ((x + y) % 8)) & 1) != 0;
230 let horizontal = y == 3 || y == 6 || y == 9;
231 let vertical = x == 2 || x == 5;
232 alpha[y * width as usize + x] = if border || (bits && (horizontal || vertical)) {
233 255
234 } else {
235 0
236 };
237 }
238 }
239 Some(GlyphRaster::new(
240 width,
241 height,
242 width as f32 + 1.0,
243 0,
244 height as i16,
245 alpha,
246 ))
247 }
248
249 fn render_em_pixels(&self) -> f32 {
250 12.0
251 }
252}
253
254const SDF_BUFFER: u16 = 3;
260
261#[derive(Debug, Clone)]
263pub struct GlyphAtlas {
264 requested: BTreeSet<GlyphKey>,
265 entries: HashMap<GlyphKey, GlyphAtlasEntry>,
266 alpha: Vec<u8>,
267 dimensions: [u16; 2],
268 render_em_px: f32,
269}
270
271impl Default for GlyphAtlas {
272 fn default() -> Self {
273 Self {
274 requested: BTreeSet::new(),
275 entries: HashMap::new(),
276 alpha: Vec::new(),
277 dimensions: [0, 0],
278 render_em_px: 24.0,
279 }
280 }
281}
282
283impl GlyphAtlas {
284 pub fn new() -> Self {
286 Self::default()
287 }
288
289 pub fn request_text(&mut self, font_stack: &str, text: &str) {
291 for codepoint in text.chars() {
292 self.requested.insert(GlyphKey {
293 font_stack: font_stack.to_owned(),
294 codepoint,
295 });
296 }
297 }
298
299 pub fn requested(&self) -> impl Iterator<Item = &GlyphKey> {
301 self.requested.iter()
302 }
303
304 pub fn len(&self) -> usize {
306 self.requested.len()
307 }
308
309 pub fn is_empty(&self) -> bool {
311 self.requested.is_empty()
312 }
313
314 pub fn entries(&self) -> impl Iterator<Item = &GlyphAtlasEntry> {
316 self.entries.values()
317 }
318
319 pub fn get(&self, font_stack: &str, codepoint: char) -> Option<&GlyphAtlasEntry> {
321 self.entries.get(&GlyphKey {
322 font_stack: font_stack.to_owned(),
323 codepoint,
324 })
325 }
326
327 pub fn alpha(&self) -> &[u8] {
329 &self.alpha
330 }
331
332 pub fn dimensions(&self) -> [u16; 2] {
334 self.dimensions
335 }
336
337 pub fn render_em_px(&self) -> f32 {
340 self.render_em_px
341 }
342
343 pub fn load_requested(&mut self, provider: &dyn GlyphProvider) {
349 self.entries.clear();
350 self.alpha.clear();
351 self.dimensions = [0, 0];
352 self.render_em_px = provider.render_em_pixels();
353
354 let mut rasters: Vec<(GlyphKey, GlyphRaster)> = Vec::new();
355 for key in &self.requested {
356 if let Some(raster) = provider.load_glyph(&key.font_stack, key.codepoint) {
357 rasters.push((key.clone(), raster));
358 }
359 }
360 if rasters.is_empty() {
361 return;
362 }
363
364 let buf = SDF_BUFFER;
366 let rasters: Vec<(GlyphKey, GlyphRaster)> = rasters
367 .into_iter()
368 .map(|(key, raster)| {
369 if raster.width == 0 || raster.height == 0 {
370 return (key, raster); }
372 let sdf_alpha = binary_to_sdf(
373 &raster.alpha,
374 raster.width as usize,
375 raster.height as usize,
376 buf as usize,
377 );
378 (
379 key,
380 GlyphRaster::new(
381 raster.width + buf * 2,
382 raster.height + buf * 2,
383 raster.advance_x,
384 raster.bearing_x - buf as i16,
385 raster.bearing_y + buf as i16,
386 sdf_alpha,
387 ),
388 )
389 })
390 .collect();
391
392 let padding = 1u16;
393 let atlas_width = rasters
394 .iter()
395 .map(|(_, raster)| raster.width + padding)
396 .sum::<u16>()
397 .max(1);
398 let atlas_height = rasters
399 .iter()
400 .map(|(_, raster)| raster.height)
401 .max()
402 .unwrap_or(0)
403 + padding * 2;
404
405 self.dimensions = [atlas_width, atlas_height.max(1)];
406 self.alpha = vec![0; atlas_width as usize * self.dimensions[1] as usize];
407
408 let mut cursor_x = padding;
409 for (key, raster) in rasters {
410 let atlas_key = key.clone();
411 let origin = [cursor_x, padding];
412 blit_alpha(
413 &mut self.alpha,
414 self.dimensions[0] as usize,
415 origin,
416 raster.width as usize,
417 raster.height as usize,
418 &raster.alpha,
419 );
420 self.entries.insert(
421 atlas_key,
422 GlyphAtlasEntry {
423 key,
424 origin,
425 size: [raster.width, raster.height],
426 advance_x: raster.advance_x,
427 bearing_x: raster.bearing_x,
428 bearing_y: raster.bearing_y,
429 },
430 );
431 cursor_x += raster.width + padding;
432 }
433 }
434}
435
436#[derive(Debug, Clone, PartialEq, Eq)]
438pub struct SpriteImage {
439 pub id: String,
441 pub origin: [u32; 2],
443 pub pixel_size: [u32; 2],
445}
446
447impl SpriteImage {
448 pub fn new(id: impl Into<String>, pixel_size: [u32; 2]) -> Self {
450 Self {
451 id: id.into(),
452 origin: [0, 0],
453 pixel_size,
454 }
455 }
456
457 pub fn with_origin(mut self, origin: [u32; 2]) -> Self {
459 self.origin = origin;
460 self
461 }
462}
463
464#[derive(Debug, Clone, Default)]
466pub struct SpriteSheet {
467 images: HashMap<String, SpriteImage>,
468}
469
470impl SpriteSheet {
471 pub fn new() -> Self {
473 Self::default()
474 }
475
476 pub fn register(&mut self, image: SpriteImage) {
478 self.images.insert(image.id.clone(), image);
479 }
480
481 pub fn get(&self, id: &str) -> Option<&SpriteImage> {
483 self.images.get(id)
484 }
485
486 pub fn iter(&self) -> impl Iterator<Item = &SpriteImage> {
488 self.images.values()
489 }
490
491 #[cfg(feature = "style-json")]
493 pub fn from_index_json(json: &str) -> Result<Self, SpriteSheetParseError> {
494 use serde::Deserialize;
495
496 #[derive(Deserialize)]
497 struct SpriteIndexEntry {
498 x: u32,
499 y: u32,
500 width: u32,
501 height: u32,
502 }
503
504 let index: HashMap<String, SpriteIndexEntry> =
505 serde_json::from_str(json).map_err(SpriteSheetParseError::Json)?;
506 let mut sheet = SpriteSheet::new();
507 for (id, entry) in index {
508 sheet.register(
509 SpriteImage::new(id, [entry.width, entry.height]).with_origin([entry.x, entry.y]),
510 );
511 }
512 Ok(sheet)
513 }
514}
515
516#[cfg(feature = "style-json")]
518#[derive(Debug)]
519pub enum SpriteSheetParseError {
520 Json(serde_json::Error),
522}
523
524#[cfg(feature = "style-json")]
525impl std::fmt::Display for SpriteSheetParseError {
526 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
527 match self {
528 SpriteSheetParseError::Json(err) => write!(f, "sprite sheet parse error: {err}"),
529 }
530 }
531}
532
533#[cfg(feature = "style-json")]
534impl std::error::Error for SpriteSheetParseError {}
535
536#[derive(Debug, Clone, Default)]
538pub struct ImageManager {
539 sprites: SpriteSheet,
540 images: HashMap<String, SpriteImage>,
541 referenced: BTreeSet<String>,
542}
543
544impl ImageManager {
545 pub fn new() -> Self {
547 Self::default()
548 }
549
550 pub fn register_sprite(&mut self, image: SpriteImage) {
552 self.sprites.register(image);
553 }
554
555 pub fn register_image(&mut self, image: SpriteImage) {
557 self.images.insert(image.id.clone(), image);
558 }
559
560 #[cfg(feature = "style-json")]
562 pub fn load_sprite_sheet_index_json(
563 &mut self,
564 json: &str,
565 ) -> Result<(), SpriteSheetParseError> {
566 self.sprites = SpriteSheet::from_index_json(json)?;
567 Ok(())
568 }
569
570 pub fn request(&mut self, id: &str) {
572 self.referenced.insert(id.to_owned());
573 }
574
575 pub fn contains(&self, id: &str) -> bool {
577 self.images.contains_key(id) || self.sprites.get(id).is_some()
578 }
579
580 pub fn referenced(&self) -> impl Iterator<Item = &str> {
582 self.referenced.iter().map(String::as_str)
583 }
584
585 fn clear_requests(&mut self) {
586 self.referenced.clear();
587 }
588}
589
590#[derive(Debug, Clone, Default, PartialEq, Eq)]
592pub struct SymbolAssetDependencies {
593 pub glyphs: BTreeSet<GlyphKey>,
595 pub images: BTreeSet<String>,
597}
598
599#[derive(Debug, Clone, Default)]
601pub struct SymbolAssetRegistry {
602 glyphs: GlyphAtlas,
603 images: ImageManager,
604}
605
606impl SymbolAssetRegistry {
607 pub fn new() -> Self {
609 Self::default()
610 }
611
612 pub fn glyphs(&self) -> &GlyphAtlas {
614 &self.glyphs
615 }
616
617 pub fn images(&self) -> &ImageManager {
619 &self.images
620 }
621
622 pub fn images_mut(&mut self) -> &mut ImageManager {
624 &mut self.images
625 }
626
627 pub fn rebuild_from_symbols(&mut self, symbols: &[PlacedSymbol]) {
629 self.glyphs = GlyphAtlas::default();
630 self.images.clear_requests();
631 for symbol in symbols.iter().filter(|symbol| symbol.visible) {
632 if let Some(text) = &symbol.text {
633 self.glyphs.request_text(&symbol.font_stack, text);
634 }
635 if let Some(icon) = &symbol.icon_image {
636 self.images.request(icon);
637 }
638 }
639 }
640}
641
642#[derive(Debug, Clone)]
644pub struct SymbolCandidate {
645 pub id: String,
647 pub layer_id: Option<String>,
649 pub source_id: Option<String>,
651 pub source_layer: Option<String>,
653 pub source_tile: Option<TileId>,
655 pub feature_id: String,
657 pub feature_index: usize,
659 pub placement_group_id: String,
666 pub placement: SymbolPlacement,
668 pub anchor: GeoCoord,
670 pub text: Option<String>,
672 pub icon_image: Option<String>,
674 pub font_stack: String,
676 pub cross_tile_id: String,
678 pub rotation_rad: f32,
684 pub size_px: f32,
686 pub padding_px: f32,
688 pub allow_overlap: bool,
690 pub ignore_placement: bool,
692 pub sort_key: Option<f32>,
697 pub radial_offset: Option<f32>,
699 pub variable_anchor_offsets: Option<Vec<(SymbolAnchor, [f32; 2])>>,
701 pub text_max_width: Option<f32>,
703 pub text_line_height: Option<f32>,
705 pub text_letter_spacing: Option<f32>,
707 pub icon_text_fit: SymbolIconTextFit,
709 pub icon_text_fit_padding: [f32; 4],
711 pub anchors: Vec<SymbolAnchor>,
713 pub writing_mode: SymbolWritingMode,
715 pub offset_px: [f32; 2],
717 pub fill_color: [f32; 4],
719 pub halo_color: [f32; 4],
721}
722
723impl SymbolCandidate {
724 pub fn dependencies(&self) -> SymbolAssetDependencies {
726 let mut deps = SymbolAssetDependencies::default();
727 if let Some(text) = &self.text {
728 for codepoint in text.chars() {
729 deps.glyphs.insert(GlyphKey {
730 font_stack: self.font_stack.clone(),
731 codepoint,
732 });
733 }
734 }
735 if let Some(icon) = &self.icon_image {
736 deps.images.insert(icon.clone());
737 }
738 deps
739 }
740
741 fn dedupe_key(&self, meters_per_pixel: f64) -> String {
742 if !self.cross_tile_id.is_empty() {
743 return self.cross_tile_id.clone();
744 }
745 cross_tile_id_for_symbol(
746 self.text.as_deref(),
747 self.icon_image.as_deref(),
748 &self.anchor,
749 meters_per_pixel,
750 )
751 }
752}
753
754#[derive(Debug, Clone, PartialEq)]
756pub struct SymbolCollisionBox {
757 pub min: [f64; 2],
759 pub max: [f64; 2],
761}
762
763impl SymbolCollisionBox {
764 pub fn intersects(&self, other: &Self) -> bool {
766 !(self.max[0] <= other.min[0]
767 || self.min[0] >= other.max[0]
768 || self.max[1] <= other.min[1]
769 || self.min[1] >= other.max[1])
770 }
771}
772
773#[derive(Debug, Clone, PartialEq)]
778pub struct GlyphQuad {
779 pub codepoint: char,
781 pub x: f32,
783 pub y: f32,
785}
786
787#[derive(Debug, Clone)]
789pub struct PlacedSymbol {
790 pub id: String,
792 pub layer_id: Option<String>,
794 pub source_id: Option<String>,
796 pub source_layer: Option<String>,
798 pub source_tile: Option<TileId>,
800 pub feature_id: String,
802 pub feature_index: usize,
804 pub placement: SymbolPlacement,
806 pub anchor: GeoCoord,
808 pub world_anchor: [f64; 3],
810 pub text: Option<String>,
812 pub icon_image: Option<String>,
814 pub font_stack: String,
816 pub cross_tile_id: String,
818 pub rotation_rad: f32,
820 pub collision_box: SymbolCollisionBox,
822 pub anchor_mode: SymbolAnchor,
824 pub writing_mode: SymbolWritingMode,
826 pub offset_px: [f32; 2],
828 pub radial_offset: Option<f32>,
830 pub text_max_width: Option<f32>,
832 pub text_line_height: Option<f32>,
834 pub text_letter_spacing: Option<f32>,
836 pub icon_text_fit: SymbolIconTextFit,
838 pub icon_text_fit_padding: [f32; 4],
840 pub size_px: f32,
842 pub fill_color: [f32; 4],
844 pub halo_color: [f32; 4],
846 pub opacity: f32,
848 pub visible: bool,
850 pub glyph_quads: Vec<GlyphQuad>,
856}
857
858impl PlacedSymbol {
859 pub fn dependencies(&self) -> SymbolAssetDependencies {
861 SymbolCandidate {
862 id: self.id.clone(),
863 layer_id: self.layer_id.clone(),
864 source_id: self.source_id.clone(),
865 source_layer: self.source_layer.clone(),
866 source_tile: self.source_tile,
867 feature_id: self.feature_id.clone(),
868 feature_index: self.feature_index,
869 placement_group_id: self.id.clone(),
870 placement: self.placement,
871 anchor: self.anchor,
872 text: self.text.clone(),
873 icon_image: self.icon_image.clone(),
874 font_stack: self.font_stack.clone(),
875 cross_tile_id: self.cross_tile_id.clone(),
876 rotation_rad: self.rotation_rad,
877 size_px: 0.0,
878 padding_px: 0.0,
879 allow_overlap: true,
880 ignore_placement: false,
881 sort_key: None,
882 radial_offset: self.radial_offset,
883 variable_anchor_offsets: None,
884 text_max_width: self.text_max_width,
885 text_line_height: self.text_line_height,
886 text_letter_spacing: self.text_letter_spacing,
887 icon_text_fit: self.icon_text_fit,
888 icon_text_fit_padding: self.icon_text_fit_padding,
889 anchors: vec![self.anchor_mode],
890 writing_mode: self.writing_mode,
891 offset_px: self.offset_px,
892 fill_color: self.fill_color,
893 halo_color: self.halo_color,
894 }
895 .dependencies()
896 }
897}
898
899#[derive(Debug, Clone)]
901pub struct SymbolPlacementConfig {
902 pub fade_in_per_second: f32,
904 pub fade_out_per_second: f32,
906 pub viewport_padding_factor: f64,
908}
909
910impl Default for SymbolPlacementConfig {
911 fn default() -> Self {
912 Self {
913 fade_in_per_second: 6.0,
914 fade_out_per_second: 8.0,
915 viewport_padding_factor: 2.0,
916 }
917 }
918}
919
920#[derive(Debug, Clone, Default)]
922pub struct SymbolPlacementEngine {
923 pub config: SymbolPlacementConfig,
925 previous_opacity: HashMap<String, f32>,
926 previous_anchor: HashMap<String, SymbolAnchor>,
927 pub cross_tile_index: cross_tile_index::CrossTileSymbolIndex,
929}
930
931impl SymbolPlacementEngine {
932 pub fn new() -> Self {
934 Self::default()
935 }
936
937 pub fn remove_tile(&mut self, tile_id: &TileId) {
942 self.cross_tile_index.remove_tile(tile_id);
943 }
944
945 pub fn place_candidates(
947 &mut self,
948 candidates: &[SymbolCandidate],
949 projection: CameraProjection,
950 meters_per_pixel: f64,
951 dt_seconds: f64,
952 viewport_bounds: Option<&WorldBounds>,
953 ) -> Vec<PlacedSymbol> {
954 let dt = dt_seconds.max(0.0) as f32;
955 let grouped_candidates = Self::group_symbol_candidates(candidates, meters_per_pixel);
956 let mut result = Vec::new();
957 let mut accepted_boxes = Vec::new();
958 let mut seen_ids = HashSet::new();
959 let mut dedupe = HashSet::new();
960
961 for (placement_id, variants) in grouped_candidates {
962 let Some(primary_candidate) = variants.first() else {
963 continue;
964 };
965 seen_ids.insert(placement_id.clone());
966 if !dedupe.insert(placement_id.clone()) {
967 continue;
968 }
969
970 let anchors = ordered_candidate_anchors(
971 primary_candidate,
972 self.previous_anchor.get(&placement_id).copied(),
973 );
974 let mut chosen = None;
975 for candidate in &variants {
976 if candidate.text.is_none() && candidate.icon_image.is_none() {
977 continue;
978 }
979 for anchor in anchors.iter().copied() {
980 let collision_boxes =
981 candidate_collision_boxes(candidate, projection, anchor, meters_per_pixel);
982 if collision_boxes.is_empty()
983 || !collision_boxes.iter().any(|bbox| {
984 viewport_contains_box(
985 viewport_bounds,
986 bbox,
987 self.config.viewport_padding_factor,
988 )
989 })
990 {
991 continue;
992 }
993 let collides = !candidate.allow_overlap
994 && accepted_boxes
995 .iter()
996 .any(|other| collision_boxes.iter().any(|bbox| bbox.intersects(other)));
997 if !collides {
998 chosen = Some((candidate, anchor, collision_boxes));
999 break;
1000 }
1001 }
1002 if chosen.is_some() {
1003 break;
1004 }
1005 }
1006
1007 let prev = self
1008 .previous_opacity
1009 .get(&placement_id)
1010 .copied()
1011 .unwrap_or(0.0);
1012 let opacity = if chosen.is_none() {
1013 (prev - self.config.fade_out_per_second * dt).max(0.0)
1014 } else {
1015 (prev + self.config.fade_in_per_second * dt).clamp(0.0, 1.0)
1016 };
1017
1018 self.previous_opacity.insert(placement_id.clone(), opacity);
1019 if opacity <= 0.0 {
1020 continue;
1021 }
1022
1023 let (selected_candidate, anchor_mode, collision_box, visible) =
1024 if let Some((selected_candidate, anchor_mode, collision_boxes)) = chosen {
1025 if !selected_candidate.ignore_placement {
1026 accepted_boxes.extend(collision_boxes.iter().cloned());
1027 }
1028 self.previous_anchor
1029 .insert(placement_id.clone(), anchor_mode);
1030 (
1031 selected_candidate,
1032 anchor_mode,
1033 merge_collision_boxes(&collision_boxes),
1034 true,
1035 )
1036 } else {
1037 let fallback_anchor = self
1038 .previous_anchor
1039 .get(&placement_id)
1040 .copied()
1041 .unwrap_or_else(|| {
1042 primary_candidate
1043 .anchors
1044 .first()
1045 .copied()
1046 .unwrap_or(SymbolAnchor::Center)
1047 });
1048 let collision_boxes = candidate_collision_boxes(
1049 primary_candidate,
1050 projection,
1051 fallback_anchor,
1052 meters_per_pixel,
1053 );
1054 (
1055 primary_candidate,
1056 fallback_anchor,
1057 merge_collision_boxes(&collision_boxes),
1058 false,
1059 )
1060 };
1061
1062 let world = projection.project(&selected_candidate.anchor);
1063 result.push(PlacedSymbol {
1064 id: selected_candidate.id.clone(),
1065 layer_id: selected_candidate.layer_id.clone(),
1066 source_id: selected_candidate.source_id.clone(),
1067 source_layer: selected_candidate.source_layer.clone(),
1068 source_tile: selected_candidate.source_tile,
1069 feature_id: selected_candidate.feature_id.clone(),
1070 feature_index: selected_candidate.feature_index,
1071 placement: selected_candidate.placement,
1072 anchor: selected_candidate.anchor,
1073 world_anchor: [world.position.x, world.position.y, world.position.z],
1074 text: selected_candidate.text.clone(),
1075 icon_image: selected_candidate.icon_image.clone(),
1076 font_stack: selected_candidate.font_stack.clone(),
1077 cross_tile_id: placement_id.clone(),
1078 rotation_rad: selected_candidate.rotation_rad,
1079 collision_box,
1080 anchor_mode,
1081 writing_mode: selected_candidate.writing_mode,
1082 offset_px: resolve_symbol_offset(
1083 anchor_mode,
1084 selected_candidate.offset_px,
1085 selected_candidate.radial_offset,
1086 selected_candidate.variable_anchor_offsets.as_deref(),
1087 selected_candidate.size_px,
1088 ),
1089 radial_offset: None,
1090 text_max_width: selected_candidate.text_max_width,
1091 text_line_height: selected_candidate.text_line_height,
1092 text_letter_spacing: selected_candidate.text_letter_spacing,
1093 icon_text_fit: selected_candidate.icon_text_fit,
1094 icon_text_fit_padding: selected_candidate.icon_text_fit_padding,
1095 size_px: selected_candidate.size_px,
1096 fill_color: selected_candidate.fill_color,
1097 halo_color: selected_candidate.halo_color,
1098 opacity,
1099 visible,
1100 glyph_quads: Vec::new(),
1101 });
1102 }
1103
1104 self.previous_opacity
1105 .retain(|id, opacity| seen_ids.contains(id) && *opacity > 0.0);
1106 self.previous_anchor.retain(|id, _| seen_ids.contains(id));
1107 result
1108 }
1109
1110 fn group_symbol_candidates<'a>(
1118 candidates: &'a [SymbolCandidate],
1119 meters_per_pixel: f64,
1120 ) -> Vec<(String, Vec<&'a SymbolCandidate>)> {
1121 let mut grouped: Vec<(String, Vec<&'a SymbolCandidate>)> = Vec::new();
1122 let mut group_indexes: HashMap<String, usize> = HashMap::new();
1123
1124 for candidate in candidates {
1125 let placement_id = candidate.dedupe_key(meters_per_pixel);
1126 let group_key = format!("{}|{}", placement_id, candidate.placement_group_id);
1127 if let Some(index) = group_indexes.get(&group_key).copied() {
1128 grouped[index].1.push(candidate);
1129 } else {
1130 group_indexes.insert(group_key, grouped.len());
1131 grouped.push((placement_id, vec![candidate]));
1132 }
1133 }
1134
1135 if grouped.iter().any(|(_, variants)| {
1141 variants
1142 .first()
1143 .and_then(|candidate| candidate.sort_key)
1144 .is_some()
1145 }) {
1146 grouped.sort_by(|(_, a_variants), (_, b_variants)| {
1147 let a_key = a_variants
1148 .first()
1149 .and_then(|candidate| candidate.sort_key)
1150 .unwrap_or(0.0);
1151 let b_key = b_variants
1152 .first()
1153 .and_then(|candidate| candidate.sort_key)
1154 .unwrap_or(0.0);
1155 a_key
1156 .partial_cmp(&b_key)
1157 .unwrap_or(std::cmp::Ordering::Equal)
1158 });
1159 }
1160
1161 grouped
1162 }
1163}
1164
1165pub fn symbol_mesh_from_placed_symbols(
1167 symbols: &[PlacedSymbol],
1168 projection: CameraProjection,
1169 meters_per_pixel: f64,
1170) -> crate::layers::VectorMeshData {
1171 let mut mesh = crate::layers::VectorMeshData::default();
1172 for symbol in symbols
1173 .iter()
1174 .filter(|symbol| symbol.visible && symbol.opacity > 0.0)
1175 {
1176 let [half_w, half_h] = symbol_half_extents(
1177 symbol.size_px,
1178 symbol.text.as_deref(),
1179 symbol.icon_image.as_deref(),
1180 symbol.writing_mode,
1181 symbol.placement,
1182 symbol.text_max_width,
1183 symbol.text_line_height,
1184 symbol.text_letter_spacing,
1185 symbol.icon_text_fit,
1186 symbol.icon_text_fit_padding,
1187 symbol.offset_px,
1188 1.0,
1189 );
1190 append_symbol_quad(
1191 &mut mesh,
1192 symbol,
1193 projection,
1194 half_w * meters_per_pixel * 1.35,
1195 half_h * meters_per_pixel * 1.35,
1196 symbol.halo_color,
1197 symbol.opacity,
1198 meters_per_pixel,
1199 );
1200 append_symbol_quad(
1201 &mut mesh,
1202 symbol,
1203 projection,
1204 half_w * meters_per_pixel,
1205 half_h * meters_per_pixel,
1206 symbol.fill_color,
1207 symbol.opacity,
1208 meters_per_pixel,
1209 );
1210 }
1211 mesh
1212}
1213
1214fn anchor_align(anchor: SymbolAnchor) -> (f32, f32) {
1223 let h = match anchor {
1224 SymbolAnchor::Left | SymbolAnchor::TopLeft | SymbolAnchor::BottomLeft => 0.0,
1225 SymbolAnchor::Right | SymbolAnchor::TopRight | SymbolAnchor::BottomRight => 1.0,
1226 _ => 0.5,
1227 };
1228 let v = match anchor {
1229 SymbolAnchor::Top | SymbolAnchor::TopLeft | SymbolAnchor::TopRight => 0.0,
1230 SymbolAnchor::Bottom | SymbolAnchor::BottomLeft | SymbolAnchor::BottomRight => 1.0,
1231 _ => 0.5,
1232 };
1233 (h, v)
1234}
1235
1236pub fn layout_symbol_glyphs(symbols: &mut [PlacedSymbol], atlas: &GlyphAtlas) {
1248 let em = atlas.render_em_px();
1249 for symbol in symbols.iter_mut() {
1250 symbol.glyph_quads.clear();
1251 if !symbol.visible || symbol.opacity <= 0.0 {
1252 continue;
1253 }
1254 let text = match &symbol.text {
1255 Some(t) if !t.is_empty() => t.clone(),
1256 _ => continue,
1257 };
1258
1259 let scale = symbol.size_px / em.max(1.0);
1260 let letter_spacing = symbol.text_letter_spacing.unwrap_or(0.0) * symbol.size_px;
1261 let line_height = symbol.text_line_height.unwrap_or(1.2) * symbol.size_px;
1262
1263 let max_line_width_px = if symbol.placement == SymbolPlacement::Point {
1265 symbol.text_max_width.map(|w| w * symbol.size_px)
1266 } else {
1267 None
1268 };
1269
1270 let lines = break_text_simple(
1271 &text,
1272 atlas,
1273 &symbol.font_stack,
1274 scale,
1275 letter_spacing,
1276 max_line_width_px,
1277 );
1278 if lines.is_empty() {
1279 continue;
1280 }
1281
1282 let line_widths: Vec<f32> = lines
1284 .iter()
1285 .map(|glyphs| glyphs.last().map(|(_, x, adv)| x + adv).unwrap_or(0.0))
1286 .collect();
1287 let max_width = line_widths.iter().cloned().fold(0.0f32, f32::max);
1288 let total_height = lines.len() as f32 * line_height;
1289
1290 let (h_align, v_align) = anchor_align(symbol.anchor_mode);
1292
1293 let mut quads = Vec::new();
1294 for (line_idx, (glyphs, line_w)) in lines.iter().zip(line_widths.iter()).enumerate() {
1295 let line_shift_x = (max_width - line_w) * 0.5;
1297 let anchor_x = -max_width * h_align;
1299 let anchor_y = -total_height * v_align + line_idx as f32 * line_height;
1301
1302 for &(codepoint, glyph_x, _advance) in glyphs {
1303 quads.push(GlyphQuad {
1304 codepoint,
1305 x: anchor_x + line_shift_x + glyph_x,
1306 y: anchor_y,
1307 });
1308 }
1309 }
1310
1311 symbol.glyph_quads = quads;
1312 }
1313}
1314
1315fn break_text_simple(
1318 text: &str,
1319 atlas: &GlyphAtlas,
1320 font_stack: &str,
1321 scale: f32,
1322 letter_spacing: f32,
1323 max_width: Option<f32>,
1324) -> Vec<Vec<(char, f32, f32)>> {
1325 let chars: Vec<char> = text.chars().collect();
1326 if chars.is_empty() {
1327 return Vec::new();
1328 }
1329
1330 let mut advances: Vec<f32> = Vec::with_capacity(chars.len());
1332 for &ch in &chars {
1333 let adv = atlas
1334 .get(font_stack, ch)
1335 .map(|e| e.advance_x * scale)
1336 .unwrap_or(0.0);
1337 advances.push(adv);
1338 }
1339
1340 let mut lines: Vec<Vec<(char, f32, f32)>> = Vec::new();
1342 let mut current_line: Vec<(char, f32, f32)> = Vec::new();
1343 let mut cursor_x: f32 = 0.0;
1344
1345 for (i, &ch) in chars.iter().enumerate() {
1346 let adv = advances[i];
1347
1348 if ch == '\n' {
1350 lines.push(std::mem::take(&mut current_line));
1351 cursor_x = 0.0;
1352 continue;
1353 }
1354
1355 if let Some(max_w) = max_width {
1357 if ch == ' ' && cursor_x > 0.0 {
1358 let next_word_end = next_word_width(&chars, &advances, i + 1, letter_spacing);
1360 if cursor_x + adv + letter_spacing + next_word_end > max_w
1361 && !current_line.is_empty()
1362 {
1363 lines.push(std::mem::take(&mut current_line));
1364 cursor_x = 0.0;
1365 continue; }
1367 }
1368 }
1369
1370 if !current_line.is_empty() {
1371 cursor_x += letter_spacing;
1372 }
1373 current_line.push((ch, cursor_x, adv));
1374 cursor_x += adv;
1375 }
1376 if !current_line.is_empty() {
1377 lines.push(current_line);
1378 }
1379
1380 lines
1381}
1382
1383fn next_word_width(chars: &[char], advances: &[f32], start: usize, letter_spacing: f32) -> f32 {
1385 let mut w: f32 = 0.0;
1386 let mut first = true;
1387 for i in start..chars.len() {
1388 if chars[i] == ' ' || chars[i] == '\n' {
1389 break;
1390 }
1391 if !first {
1392 w += letter_spacing;
1393 }
1394 w += advances[i];
1395 first = false;
1396 }
1397 w
1398}
1399
1400#[cfg(feature = "text-shaping")]
1407pub fn layout_symbol_glyphs_shaped(
1408 symbols: &mut [PlacedSymbol],
1409 registry: &mut text_shaper::FontRegistry,
1410) {
1411 use text_shaper::{shape_text, ShapeTextOptions, TextAnchor, TextJustify, ONE_EM};
1412
1413 for symbol in symbols.iter_mut() {
1414 symbol.glyph_quads.clear();
1415 if !symbol.visible || symbol.opacity <= 0.0 {
1416 continue;
1417 }
1418 let text = match &symbol.text {
1419 Some(t) if !t.is_empty() => t.as_str(),
1420 _ => continue,
1421 };
1422
1423 let anchor = match symbol.anchor_mode {
1424 SymbolAnchor::Center => TextAnchor::Center,
1425 SymbolAnchor::Top => TextAnchor::Top,
1426 SymbolAnchor::Bottom => TextAnchor::Bottom,
1427 SymbolAnchor::Left => TextAnchor::Left,
1428 SymbolAnchor::Right => TextAnchor::Right,
1429 SymbolAnchor::TopLeft => TextAnchor::TopLeft,
1430 SymbolAnchor::TopRight => TextAnchor::TopRight,
1431 SymbolAnchor::BottomLeft => TextAnchor::BottomLeft,
1432 SymbolAnchor::BottomRight => TextAnchor::BottomRight,
1433 };
1434
1435 let options = ShapeTextOptions {
1436 font_stack: symbol.font_stack.clone(),
1437 max_width: if symbol.placement == SymbolPlacement::Point {
1438 symbol.text_max_width.or(Some(10.0))
1439 } else {
1440 None
1441 },
1442 line_height: symbol.text_line_height.unwrap_or(1.2),
1443 letter_spacing: symbol.text_letter_spacing.unwrap_or(0.0),
1444 justify: TextJustify::Center,
1445 anchor,
1446 writing_mode: symbol.writing_mode,
1447 text_transform: SymbolTextTransform::None,
1448 };
1449
1450 let shaped = match shape_text(text, registry, &options) {
1451 Some(s) => s,
1452 None => continue,
1453 };
1454
1455 let px_per_layout = symbol.size_px / ONE_EM;
1456 let mut quads = Vec::with_capacity(shaped.glyph_count());
1457 for line in &shaped.lines {
1458 for glyph in &line.glyphs {
1459 quads.push(GlyphQuad {
1460 codepoint: glyph.codepoint,
1461 x: glyph.x * px_per_layout,
1462 y: glyph.y * px_per_layout,
1463 });
1464 }
1465 }
1466
1467 symbol.glyph_quads = quads;
1468 }
1469}
1470
1471fn candidate_collision_boxes(
1472 candidate: &SymbolCandidate,
1473 projection: CameraProjection,
1474 anchor_mode: SymbolAnchor,
1475 meters_per_pixel: f64,
1476) -> Vec<SymbolCollisionBox> {
1477 let world = projection.project(&candidate.anchor);
1478 let effective_offset = resolve_symbol_offset(
1479 anchor_mode,
1480 candidate.offset_px,
1481 candidate.radial_offset,
1482 candidate.variable_anchor_offsets.as_deref(),
1483 candidate.size_px,
1484 );
1485 let [half_w_px, half_h_px] = symbol_half_extents(
1486 candidate.size_px,
1487 candidate.text.as_deref(),
1488 candidate.icon_image.as_deref(),
1489 candidate.writing_mode,
1490 candidate.placement,
1491 candidate.text_max_width,
1492 candidate.text_line_height,
1493 candidate.text_letter_spacing,
1494 candidate.icon_text_fit,
1495 candidate.icon_text_fit_padding,
1496 effective_offset,
1497 candidate.padding_px.max(0.0) as f64,
1498 );
1499 let half_w = half_w_px * meters_per_pixel;
1500 let half_h = half_h_px * meters_per_pixel;
1501 let signs = anchor_mode.offset_signs();
1502 let center_x =
1503 world.position.x + effective_offset[0] as f64 * meters_per_pixel + signs[0] * half_w * 2.0;
1504 let center_y =
1505 world.position.y + effective_offset[1] as f64 * meters_per_pixel + signs[1] * half_h * 2.0;
1506
1507 segmented_collision_boxes(candidate, center_x, center_y, half_w, half_h)
1508}
1509
1510fn segmented_collision_boxes(
1518 candidate: &SymbolCandidate,
1519 center_x: f64,
1520 center_y: f64,
1521 half_w: f64,
1522 half_h: f64,
1523) -> Vec<SymbolCollisionBox> {
1524 if candidate.placement != SymbolPlacement::Line {
1525 return vec![rotated_collision_box(
1526 center_x,
1527 center_y,
1528 half_w,
1529 half_h,
1530 candidate.rotation_rad,
1531 )];
1532 }
1533
1534 let full_width = half_w * 2.0;
1535 let full_height = half_h * 2.0;
1536 let segment_count = ((full_width / full_height.max(1.0)).ceil() as usize).clamp(1, 4);
1537 let segment_half_w = half_w / segment_count as f64;
1538 let sin_theta = candidate.rotation_rad.sin() as f64;
1539 let cos_theta = candidate.rotation_rad.cos() as f64;
1540 let mut boxes = Vec::with_capacity(segment_count);
1541
1542 for index in 0..segment_count {
1543 let local_center_x = -half_w + segment_half_w + (index as f64 * segment_half_w * 2.0);
1544 let rotated_center_x = local_center_x * cos_theta;
1545 let rotated_center_y = local_center_x * sin_theta;
1546 boxes.push(rotated_collision_box(
1547 center_x + rotated_center_x,
1548 center_y + rotated_center_y,
1549 segment_half_w,
1550 half_h,
1551 candidate.rotation_rad,
1552 ));
1553 }
1554
1555 boxes
1556}
1557
1558fn rotated_collision_box(
1565 center_x: f64,
1566 center_y: f64,
1567 half_w: f64,
1568 half_h: f64,
1569 rotation_rad: f32,
1570) -> SymbolCollisionBox {
1571 let sin_theta = rotation_rad.sin() as f64;
1572 let cos_theta = rotation_rad.cos() as f64;
1573 let mut min_x = f64::INFINITY;
1574 let mut min_y = f64::INFINITY;
1575 let mut max_x = f64::NEG_INFINITY;
1576 let mut max_y = f64::NEG_INFINITY;
1577
1578 for [local_x, local_y] in [
1579 [-half_w, -half_h],
1580 [half_w, -half_h],
1581 [half_w, half_h],
1582 [-half_w, half_h],
1583 ] {
1584 let rotated_x = local_x * cos_theta - local_y * sin_theta;
1585 let rotated_y = local_x * sin_theta + local_y * cos_theta;
1586 let x = center_x + rotated_x;
1587 let y = center_y + rotated_y;
1588 min_x = min_x.min(x);
1589 min_y = min_y.min(y);
1590 max_x = max_x.max(x);
1591 max_y = max_y.max(y);
1592 }
1593
1594 SymbolCollisionBox {
1595 min: [min_x, min_y],
1596 max: [max_x, max_y],
1597 }
1598}
1599
1600fn merge_collision_boxes(boxes: &[SymbolCollisionBox]) -> SymbolCollisionBox {
1601 let mut min_x = f64::INFINITY;
1602 let mut min_y = f64::INFINITY;
1603 let mut max_x = f64::NEG_INFINITY;
1604 let mut max_y = f64::NEG_INFINITY;
1605
1606 for bbox in boxes {
1607 min_x = min_x.min(bbox.min[0]);
1608 min_y = min_y.min(bbox.min[1]);
1609 max_x = max_x.max(bbox.max[0]);
1610 max_y = max_y.max(bbox.max[1]);
1611 }
1612
1613 if boxes.is_empty() {
1614 SymbolCollisionBox {
1615 min: [0.0, 0.0],
1616 max: [0.0, 0.0],
1617 }
1618 } else {
1619 SymbolCollisionBox {
1620 min: [min_x, min_y],
1621 max: [max_x, max_y],
1622 }
1623 }
1624}
1625
1626fn ordered_candidate_anchors(
1627 candidate: &SymbolCandidate,
1628 previous: Option<SymbolAnchor>,
1629) -> Vec<SymbolAnchor> {
1630 let mut anchors = candidate.anchors.clone();
1631 if anchors.is_empty() {
1632 anchors.push(SymbolAnchor::Center);
1633 }
1634 if let Some(previous) = previous {
1635 if let Some(index) = anchors.iter().position(|anchor| *anchor == previous) {
1636 anchors.swap(0, index);
1637 }
1638 }
1639 anchors
1640}
1641
1642fn viewport_contains_box(
1643 viewport_bounds: Option<&WorldBounds>,
1644 bbox: &SymbolCollisionBox,
1645 padding_factor: f64,
1646) -> bool {
1647 let Some(bounds) = viewport_bounds else {
1648 return true;
1649 };
1650 let width = (bbox.max[0] - bbox.min[0]).abs() * padding_factor;
1651 let height = (bbox.max[1] - bbox.min[1]).abs() * padding_factor;
1652 !(bbox.max[0] < bounds.min.position.x - width
1653 || bbox.min[0] > bounds.max.position.x + width
1654 || bbox.max[1] < bounds.min.position.y - height
1655 || bbox.min[1] > bounds.max.position.y + height)
1656}
1657
1658#[allow(clippy::too_many_arguments)]
1659fn symbol_half_extents(
1660 size_px: f32,
1661 text: Option<&str>,
1662 icon_image: Option<&str>,
1663 writing_mode: SymbolWritingMode,
1664 placement: SymbolPlacement,
1665 text_max_width: Option<f32>,
1666 text_line_height: Option<f32>,
1667 text_letter_spacing: Option<f32>,
1668 icon_text_fit: SymbolIconTextFit,
1669 icon_text_fit_padding: [f32; 4],
1670 offset_px: [f32; 2],
1671 padding_px: f64,
1672) -> [f64; 2] {
1673 let size_px = size_px.max(1.0) as f64;
1674 let (text_width_px, text_height_px) = estimate_text_box(
1675 text,
1676 size_px,
1677 placement,
1678 text_max_width,
1679 text_line_height,
1680 text_letter_spacing,
1681 );
1682 let icon_size_px = if icon_image.is_some() {
1683 size_px * 1.2
1684 } else {
1685 0.0
1686 };
1687 let (width_px, height_px) = match writing_mode {
1688 SymbolWritingMode::Horizontal => fitted_symbol_box(
1689 text_width_px.max(size_px),
1690 text_height_px.max(size_px),
1691 icon_image.is_some(),
1692 icon_size_px,
1693 icon_text_fit,
1694 icon_text_fit_padding,
1695 padding_px,
1696 ),
1697 SymbolWritingMode::Vertical => fitted_symbol_box(
1698 text_height_px.max(size_px),
1699 text_width_px.max(size_px),
1700 icon_image.is_some(),
1701 icon_size_px,
1702 icon_text_fit,
1703 icon_text_fit_padding,
1704 padding_px,
1705 ),
1706 };
1707 [
1708 (width_px + offset_px[0].abs() as f64) * 0.5,
1709 (height_px + offset_px[1].abs() as f64) * 0.5,
1710 ]
1711}
1712
1713fn estimate_text_box(
1724 text: Option<&str>,
1725 size_px: f64,
1726 placement: SymbolPlacement,
1727 text_max_width: Option<f32>,
1728 text_line_height: Option<f32>,
1729 text_letter_spacing: Option<f32>,
1730) -> (f64, f64) {
1731 let Some(text) = text else {
1732 return (0.0, 0.0);
1733 };
1734
1735 let glyph_count = text.chars().count() as f64;
1736 let letter_spacing_px = text_letter_spacing.unwrap_or(0.0).max(0.0) as f64 * size_px;
1737 let total_width_px =
1738 glyph_count * size_px * 0.6 + (glyph_count - 1.0).max(0.0) * letter_spacing_px;
1739 let line_height_px = size_px * text_line_height.unwrap_or(1.2).max(0.1) as f64;
1740
1741 if placement != SymbolPlacement::Point {
1742 return (total_width_px, line_height_px);
1743 }
1744
1745 let Some(max_width_em) = text_max_width else {
1746 return (total_width_px, line_height_px);
1747 };
1748 let max_width_px = (max_width_em.max(1.0) as f64) * size_px;
1749 if total_width_px <= max_width_px {
1750 return (total_width_px, line_height_px);
1751 }
1752
1753 let line_count = (total_width_px / max_width_px).ceil().max(1.0);
1754 (max_width_px, line_height_px * line_count)
1755}
1756
1757fn fitted_symbol_box(
1764 text_width_px: f64,
1765 text_height_px: f64,
1766 has_icon: bool,
1767 icon_size_px: f64,
1768 icon_text_fit: SymbolIconTextFit,
1769 icon_text_fit_padding: [f32; 4],
1770 padding_px: f64,
1771) -> (f64, f64) {
1772 if !has_icon {
1773 return (
1774 text_width_px + padding_px * 2.0,
1775 text_height_px + padding_px * 2.0,
1776 );
1777 }
1778
1779 let icon_base_width = icon_size_px;
1780 let icon_base_height = icon_size_px;
1781 if icon_text_fit == SymbolIconTextFit::None || text_width_px <= 0.0 || text_height_px <= 0.0 {
1782 return (
1783 text_width_px + icon_base_width + padding_px * 2.0,
1784 text_height_px.max(icon_base_height) + padding_px * 2.0,
1785 );
1786 }
1787
1788 let fit_width = match icon_text_fit {
1789 SymbolIconTextFit::None | SymbolIconTextFit::Height => icon_base_width,
1790 SymbolIconTextFit::Width | SymbolIconTextFit::Both => {
1791 text_width_px + icon_text_fit_padding[1] as f64 + icon_text_fit_padding[3] as f64
1792 }
1793 };
1794 let fit_height = match icon_text_fit {
1795 SymbolIconTextFit::None | SymbolIconTextFit::Width => icon_base_height,
1796 SymbolIconTextFit::Height | SymbolIconTextFit::Both => {
1797 text_height_px + icon_text_fit_padding[0] as f64 + icon_text_fit_padding[2] as f64
1798 }
1799 };
1800
1801 (
1802 fit_width.max(text_width_px) + padding_px * 2.0,
1803 fit_height.max(text_height_px) + padding_px * 2.0,
1804 )
1805}
1806
1807fn resolve_symbol_offset(
1817 anchor_mode: SymbolAnchor,
1818 offset_px: [f32; 2],
1819 radial_offset: Option<f32>,
1820 variable_anchor_offsets: Option<&[(SymbolAnchor, [f32; 2])]>,
1821 size_px: f32,
1822) -> [f32; 2] {
1823 if let Some(anchor_offsets) = variable_anchor_offsets {
1824 if let Some((_, offset)) = anchor_offsets
1825 .iter()
1826 .find(|(anchor, _)| *anchor == anchor_mode)
1827 {
1828 return [offset[0] * size_px.max(1.0), offset[1] * size_px.max(1.0)];
1829 }
1830 }
1831 let Some(radial_offset) = radial_offset else {
1832 return resolve_anchor_relative_text_offset(anchor_mode, offset_px);
1833 };
1834
1835 let radial_px = radial_offset.max(0.0) * size_px.max(1.0);
1836 let diagonal_px = radial_px / std::f32::consts::SQRT_2;
1837 match anchor_mode {
1838 SymbolAnchor::Center => [0.0, 0.0],
1839 SymbolAnchor::Top => [0.0, radial_px],
1840 SymbolAnchor::Bottom => [0.0, -radial_px],
1841 SymbolAnchor::Left => [radial_px, 0.0],
1842 SymbolAnchor::Right => [-radial_px, 0.0],
1843 SymbolAnchor::TopLeft => [diagonal_px, diagonal_px],
1844 SymbolAnchor::TopRight => [-diagonal_px, diagonal_px],
1845 SymbolAnchor::BottomLeft => [diagonal_px, -diagonal_px],
1846 SymbolAnchor::BottomRight => [-diagonal_px, -diagonal_px],
1847 }
1848}
1849
1850fn resolve_anchor_relative_text_offset(anchor_mode: SymbolAnchor, offset_px: [f32; 2]) -> [f32; 2] {
1858 let offset_x = offset_px[0].abs();
1859 let offset_y = offset_px[1].abs();
1860
1861 match anchor_mode {
1862 SymbolAnchor::Center => offset_px,
1863 SymbolAnchor::Top => [0.0, offset_y],
1864 SymbolAnchor::Bottom => [0.0, -offset_y],
1865 SymbolAnchor::Left => [offset_x, 0.0],
1866 SymbolAnchor::Right => [-offset_x, 0.0],
1867 SymbolAnchor::TopLeft => [offset_x, offset_y],
1868 SymbolAnchor::TopRight => [-offset_x, offset_y],
1869 SymbolAnchor::BottomLeft => [offset_x, -offset_y],
1870 SymbolAnchor::BottomRight => [-offset_x, -offset_y],
1871 }
1872}
1873
1874#[allow(clippy::too_many_arguments)]
1875fn append_symbol_quad(
1876 mesh: &mut crate::layers::VectorMeshData,
1877 symbol: &PlacedSymbol,
1878 projection: CameraProjection,
1879 half_w: f64,
1880 half_h: f64,
1881 mut color: [f32; 4],
1882 opacity: f32,
1883 meters_per_pixel: f64,
1884) {
1885 color[3] *= opacity.clamp(0.0, 1.0);
1886 let scale = projection.scale_factor(&symbol.anchor).max(1e-6);
1887 let offset_scale = 1.0 / scale;
1888 let signs = symbol.anchor_mode.offset_signs();
1889 let center_x = symbol.world_anchor[0]
1890 + symbol.offset_px[0] as f64 * meters_per_pixel * offset_scale
1891 + signs[0] * half_w * 2.0;
1892 let center_y = symbol.world_anchor[1]
1893 + symbol.offset_px[1] as f64 * meters_per_pixel * offset_scale
1894 + signs[1] * half_h * 2.0;
1895 let z = symbol.world_anchor[2];
1896 let sin_theta = symbol.rotation_rad.sin() as f64;
1897 let cos_theta = symbol.rotation_rad.cos() as f64;
1898 let base = mesh.positions.len() as u32;
1899 for [local_x, local_y] in [
1900 [-half_w, -half_h],
1901 [half_w, -half_h],
1902 [half_w, half_h],
1903 [-half_w, half_h],
1904 ] {
1905 let rotated_x = local_x * cos_theta - local_y * sin_theta;
1906 let rotated_y = local_x * sin_theta + local_y * cos_theta;
1907 mesh.positions
1908 .push([center_x + rotated_x, center_y + rotated_y, z]);
1909 }
1910 for _ in 0..4 {
1911 mesh.colors.push(color);
1912 }
1913 mesh.indices
1914 .extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
1915}
1916
1917fn cross_tile_id_for_symbol(
1918 text: Option<&str>,
1919 icon: Option<&str>,
1920 anchor: &GeoCoord,
1921 meters_per_pixel: f64,
1922) -> String {
1923 let world = CameraProjection::WebMercator.project(anchor);
1924 let bucket = (meters_per_pixel * 32.0).max(1.0);
1925 let x = (world.position.x / bucket).round() as i64;
1926 let y = (world.position.y / bucket).round() as i64;
1927 format!("{}|{}|{}|{}", text.unwrap_or(""), icon.unwrap_or(""), x, y)
1928}
1929
1930fn binary_to_sdf(alpha: &[u8], width: usize, height: usize, buffer: usize) -> Vec<u8> {
1940 if width == 0 || height == 0 {
1941 return Vec::new();
1942 }
1943 let new_w = width + 2 * buffer;
1944 let new_h = height + 2 * buffer;
1945 let len = new_w * new_h;
1946 let big = (new_w * new_w + new_h * new_h) as f32;
1947
1948 let mut outer = vec![big; len];
1951 let mut inner = vec![0.0f32; len];
1954
1955 for y in 0..height {
1956 for x in 0..width {
1957 let dst = (y + buffer) * new_w + (x + buffer);
1958 if alpha[y * width + x] > 127 {
1959 outer[dst] = 0.0;
1960 inner[dst] = big;
1961 }
1962 }
1963 }
1964
1965 edt_2d(&mut outer, new_w, new_h);
1966 edt_2d(&mut inner, new_w, new_h);
1967
1968 let buf_f = buffer as f32;
1969 let mut sdf = vec![0u8; len];
1970 for i in 0..len {
1971 let dist = outer[i].sqrt() - inner[i].sqrt();
1972 let normalized = 0.5 - dist / (2.0 * buf_f);
1973 sdf[i] = (normalized.clamp(0.0, 1.0) * 255.0) as u8;
1974 }
1975 sdf
1976}
1977
1978fn edt_2d(grid: &mut [f32], width: usize, height: usize) {
1980 for y in 0..height {
1981 let offset = y * width;
1982 edt_1d(&mut grid[offset..offset + width]);
1983 }
1984 let mut col = vec![0.0f32; height];
1985 for x in 0..width {
1986 for y in 0..height {
1987 col[y] = grid[y * width + x];
1988 }
1989 edt_1d(&mut col);
1990 for y in 0..height {
1991 grid[y * width + x] = col[y];
1992 }
1993 }
1994}
1995
1996#[allow(clippy::needless_range_loop)]
2002fn edt_1d(f: &mut [f32]) {
2003 let n = f.len();
2004 if n <= 1 {
2005 return;
2006 }
2007 let mut v = vec![0usize; n]; let mut z = vec![0.0f32; n + 1]; let mut d = vec![0.0f32; n]; z[0] = f32::NEG_INFINITY;
2012 z[1] = f32::INFINITY;
2013 let mut k: usize = 0;
2014
2015 for q in 1..n {
2016 let q2 = (q * q) as f32;
2017 loop {
2018 let vk = v[k];
2019 let vk2 = (vk * vk) as f32;
2020 let s = ((f[q] + q2) - (f[vk] + vk2)) / (2.0 * (q - vk) as f32);
2021 if s > z[k] {
2022 k += 1;
2023 v[k] = q;
2024 z[k] = s;
2025 z[k + 1] = f32::INFINITY;
2026 break;
2027 }
2028 k -= 1;
2030 }
2031 }
2032
2033 k = 0;
2034 for q in 0..n {
2035 while z[k + 1] < q as f32 {
2036 k += 1;
2037 }
2038 let dq = q as f32 - v[k] as f32;
2039 d[q] = dq * dq + f[v[k]];
2040 }
2041 f.copy_from_slice(&d);
2042}
2043
2044fn blit_alpha(
2045 atlas: &mut [u8],
2046 atlas_width: usize,
2047 origin: [u16; 2],
2048 width: usize,
2049 height: usize,
2050 src: &[u8],
2051) {
2052 for row in 0..height {
2053 let dst_start = (origin[1] as usize + row) * atlas_width + origin[0] as usize;
2054 let src_start = row * width;
2055 atlas[dst_start..dst_start + width].copy_from_slice(&src[src_start..src_start + width]);
2056 }
2057}
2058
2059#[cfg(test)]
2060mod tests {
2061 use super::*;
2062 use crate::camera_projection::CameraProjection;
2063 use rustial_math::GeoCoord;
2064
2065 fn candidate(id: &str, lon: f64, text: &str) -> SymbolCandidate {
2066 SymbolCandidate {
2067 id: id.into(),
2068 layer_id: Some("symbols".into()),
2069 source_id: Some("source".into()),
2070 source_layer: Some("poi".into()),
2071 source_tile: None,
2072 feature_id: id.into(),
2073 feature_index: 0,
2074 placement_group_id: id.into(),
2075 placement: SymbolPlacement::Point,
2076 anchor: GeoCoord::from_lat_lon(0.0, lon),
2077 text: Some(text.into()),
2078 icon_image: None,
2079 font_stack: "Test Sans".into(),
2080 cross_tile_id: id.into(),
2081 rotation_rad: 0.0,
2082 size_px: 16.0,
2083 padding_px: 2.0,
2084 allow_overlap: false,
2085 ignore_placement: false,
2086 sort_key: None,
2087 radial_offset: None,
2088 variable_anchor_offsets: None,
2089 text_max_width: None,
2090 text_line_height: None,
2091 text_letter_spacing: None,
2092 icon_text_fit: SymbolIconTextFit::None,
2093 icon_text_fit_padding: [0.0, 0.0, 0.0, 0.0],
2094 anchors: vec![SymbolAnchor::Center],
2095 writing_mode: SymbolWritingMode::Horizontal,
2096 offset_px: [0.0, 0.0],
2097 fill_color: [1.0, 1.0, 1.0, 1.0],
2098 halo_color: [0.0, 0.0, 0.0, 1.0],
2099 }
2100 }
2101
2102 #[test]
2103 fn glyph_atlas_tracks_unique_glyphs() {
2104 let mut atlas = GlyphAtlas::new();
2105 atlas.request_text("Test Sans", "aba");
2106 assert_eq!(atlas.len(), 2);
2107 atlas.load_requested(&ProceduralGlyphProvider::new());
2108 assert_eq!(atlas.entries().count(), 2);
2109 assert!(atlas.dimensions()[0] > 0);
2110 }
2111
2112 #[test]
2113 fn image_manager_tracks_referenced_ids() {
2114 let mut images = ImageManager::new();
2115 images.register_image(SpriteImage::new("marker", [32, 32]));
2116 images.request("marker");
2117 assert!(images.contains("marker"));
2118 assert_eq!(images.referenced().collect::<Vec<_>>(), vec!["marker"]);
2119 }
2120
2121 #[test]
2122 fn placement_filters_colliding_symbols() {
2123 let mut engine = SymbolPlacementEngine::new();
2124 let placed = engine.place_candidates(
2125 &[
2126 candidate("a", 0.0, "Alpha"),
2127 candidate("b", 0.00001, "Beta"),
2128 ],
2129 CameraProjection::WebMercator,
2130 2.0,
2131 1.0 / 60.0,
2132 None,
2133 );
2134 assert_eq!(placed.iter().filter(|symbol| symbol.visible).count(), 1);
2135 }
2136
2137 #[test]
2138 fn placement_dedupes_nearby_repeated_labels() {
2139 let mut engine = SymbolPlacementEngine::new();
2140 let placed = engine.place_candidates(
2141 &[candidate("a", 0.0, "Same"), candidate("b", 0.0, "Same")],
2142 CameraProjection::WebMercator,
2143 2.0,
2144 1.0 / 60.0,
2145 None,
2146 );
2147 assert_eq!(placed.len(), 1);
2148 }
2149
2150 #[test]
2151 fn placement_tries_alternate_anchors() {
2152 let mut a = candidate("a", 0.0, "Alpha");
2153 let mut b = candidate("b", 0.0, "Beta");
2154 a.anchors = vec![SymbolAnchor::Center];
2155 b.anchors = vec![SymbolAnchor::Center, SymbolAnchor::Top];
2156
2157 let mut engine = SymbolPlacementEngine::new();
2158 let placed =
2159 engine.place_candidates(&[a, b], CameraProjection::WebMercator, 2.0, 1.0, None);
2160 assert_eq!(placed.iter().filter(|symbol| symbol.visible).count(), 2);
2161 assert_eq!(placed[1].anchor_mode, SymbolAnchor::Top);
2162 }
2163
2164 #[test]
2165 fn placement_preserves_previous_anchor_by_cross_tile_id() {
2166 let mut first = candidate("a", 0.0, "Alpha");
2167 first.cross_tile_id = "shared".into();
2168 first.anchors = vec![SymbolAnchor::Center, SymbolAnchor::Top];
2169
2170 let mut blocker = candidate("blocker", 0.0, "Block");
2171 blocker.anchors = vec![SymbolAnchor::Center];
2172
2173 let mut engine = SymbolPlacementEngine::new();
2174 let placed = engine.place_candidates(
2175 &[blocker.clone(), first.clone()],
2176 CameraProjection::WebMercator,
2177 2.0,
2178 1.0,
2179 None,
2180 );
2181 assert_eq!(placed[1].anchor_mode, SymbolAnchor::Top);
2182
2183 let mut second = candidate("b", 0.0, "Alpha");
2184 second.cross_tile_id = "shared".into();
2185 second.anchors = vec![SymbolAnchor::Center, SymbolAnchor::Top];
2186 let placed = engine.place_candidates(
2187 &[blocker, second],
2188 CameraProjection::WebMercator,
2189 2.0,
2190 1.0 / 60.0,
2191 None,
2192 );
2193 assert_eq!(placed[1].anchor_mode, SymbolAnchor::Top);
2194 }
2195
2196 #[test]
2197 fn placement_honors_symbol_sort_key_order() {
2198 let mut low = candidate("low", 0.0, "Low");
2199 low.sort_key = Some(0.0);
2200
2201 let mut high = candidate("high", 0.0, "High");
2202 high.sort_key = Some(10.0);
2203
2204 let mut engine = SymbolPlacementEngine::new();
2205 let placed =
2206 engine.place_candidates(&[high, low], CameraProjection::WebMercator, 2.0, 1.0, None);
2207
2208 assert_eq!(placed.iter().filter(|symbol| symbol.visible).count(), 1);
2209 assert_eq!(
2210 placed
2211 .iter()
2212 .find(|symbol| symbol.visible)
2213 .map(|symbol| symbol.id.as_str()),
2214 Some("low")
2215 );
2216 }
2217
2218 #[test]
2219 fn fixed_offset_is_resolved_relative_to_anchor_direction() {
2220 assert_eq!(
2221 resolve_symbol_offset(SymbolAnchor::TopRight, [3.0, 4.0], None, None, 10.0),
2222 [-3.0, 4.0]
2223 );
2224 assert_eq!(
2225 resolve_symbol_offset(SymbolAnchor::BottomLeft, [3.0, 4.0], None, None, 10.0),
2226 [3.0, -4.0]
2227 );
2228 }
2229
2230 #[test]
2231 fn centered_fixed_offset_preserves_raw_vector() {
2232 assert_eq!(
2233 resolve_symbol_offset(SymbolAnchor::Center, [3.0, -4.0], None, None, 10.0),
2234 [3.0, -4.0]
2235 );
2236 }
2237
2238 #[test]
2239 fn radial_offset_overrides_fixed_offset_for_anchor_direction() {
2240 let resolved =
2241 resolve_symbol_offset(SymbolAnchor::TopRight, [99.0, 99.0], Some(2.0), None, 10.0);
2242
2243 assert!(resolved[0] < 0.0);
2244 assert!(resolved[1] > 0.0);
2245 assert!(resolved[0].abs() < 99.0);
2246 assert!(resolved[1].abs() < 99.0);
2247 }
2248
2249 #[test]
2250 fn variable_anchor_offset_overrides_radial_and_fixed_offsets() {
2251 let resolved = resolve_symbol_offset(
2252 SymbolAnchor::Top,
2253 [99.0, 99.0],
2254 Some(5.0),
2255 Some(&[(SymbolAnchor::Top, [1.0, 2.0])]),
2256 10.0,
2257 );
2258
2259 assert_eq!(resolved, [10.0, 20.0]);
2260 }
2261
2262 #[test]
2263 fn point_text_max_width_wraps_placeholder_text_box() {
2264 let single_line = estimate_text_box(
2265 Some("abcdefghij"),
2266 10.0,
2267 SymbolPlacement::Point,
2268 None,
2269 None,
2270 None,
2271 );
2272 let wrapped = estimate_text_box(
2273 Some("abcdefghij"),
2274 10.0,
2275 SymbolPlacement::Point,
2276 Some(3.0),
2277 None,
2278 None,
2279 );
2280
2281 assert!(wrapped.0 < single_line.0);
2282 assert!(wrapped.1 > single_line.1);
2283 }
2284
2285 #[test]
2286 fn line_text_max_width_does_not_wrap_placeholder_text_box() {
2287 let unbounded = estimate_text_box(
2288 Some("abcdefghij"),
2289 10.0,
2290 SymbolPlacement::Line,
2291 None,
2292 None,
2293 None,
2294 );
2295 let bounded = estimate_text_box(
2296 Some("abcdefghij"),
2297 10.0,
2298 SymbolPlacement::Line,
2299 Some(3.0),
2300 None,
2301 None,
2302 );
2303
2304 assert_eq!(bounded, unbounded);
2305 }
2306
2307 #[test]
2308 fn wrapped_text_box_uses_text_line_height() {
2309 let compact = estimate_text_box(
2310 Some("abcdefghij"),
2311 10.0,
2312 SymbolPlacement::Point,
2313 Some(3.0),
2314 Some(1.0),
2315 None,
2316 );
2317 let spacious = estimate_text_box(
2318 Some("abcdefghij"),
2319 10.0,
2320 SymbolPlacement::Point,
2321 Some(3.0),
2322 Some(2.0),
2323 None,
2324 );
2325
2326 assert_eq!(compact.0, spacious.0);
2327 assert!(spacious.1 > compact.1);
2328 }
2329
2330 #[test]
2331 fn text_letter_spacing_expands_placeholder_text_box_width() {
2332 let compact = estimate_text_box(
2333 Some("abcde"),
2334 10.0,
2335 SymbolPlacement::Point,
2336 None,
2337 None,
2338 None,
2339 );
2340 let spaced = estimate_text_box(
2341 Some("abcde"),
2342 10.0,
2343 SymbolPlacement::Point,
2344 None,
2345 None,
2346 Some(0.25),
2347 );
2348
2349 assert!(spaced.0 > compact.0);
2350 assert_eq!(spaced.1, compact.1);
2351 }
2352
2353 #[test]
2354 fn icon_text_fit_both_wraps_icon_around_text_box() {
2355 let fitted = symbol_half_extents(
2356 10.0,
2357 Some("abcdefghij"),
2358 Some("marker"),
2359 SymbolWritingMode::Horizontal,
2360 SymbolPlacement::Point,
2361 Some(3.0),
2362 Some(1.5),
2363 Some(0.25),
2364 SymbolIconTextFit::Both,
2365 [1.0, 2.0, 3.0, 4.0],
2366 [0.0, 0.0],
2367 0.0,
2368 );
2369 let unfitted = symbol_half_extents(
2370 10.0,
2371 Some("abcdefghij"),
2372 Some("marker"),
2373 SymbolWritingMode::Horizontal,
2374 SymbolPlacement::Point,
2375 Some(3.0),
2376 Some(1.5),
2377 Some(0.25),
2378 SymbolIconTextFit::None,
2379 [0.0, 0.0, 0.0, 0.0],
2380 [0.0, 0.0],
2381 0.0,
2382 );
2383
2384 assert!(fitted[0] < unfitted[0]);
2385 assert!(fitted[1] > unfitted[1]);
2386 }
2387
2388 #[test]
2389 fn icon_text_fit_width_only_keeps_base_height() {
2390 let fitted = fitted_symbol_box(
2391 40.0,
2392 18.0,
2393 true,
2394 12.0,
2395 SymbolIconTextFit::Width,
2396 [2.0, 3.0, 4.0, 5.0],
2397 0.0,
2398 );
2399
2400 assert_eq!(fitted.1, 18.0);
2401 assert_eq!(fitted.0, 48.0);
2402 }
2403
2404 #[test]
2405 fn placement_ignored_symbol_does_not_block_later_symbol() {
2406 let mut first = candidate("first", 0.0, "Alpha");
2407 first.ignore_placement = true;
2408
2409 let second = candidate("second", 0.0, "Beta");
2410
2411 let mut engine = SymbolPlacementEngine::new();
2412 let placed = engine.place_candidates(
2413 &[first, second],
2414 CameraProjection::WebMercator,
2415 2.0,
2416 1.0,
2417 None,
2418 );
2419
2420 assert_eq!(placed.iter().filter(|symbol| symbol.visible).count(), 2);
2421 }
2422
2423 #[test]
2424 fn asset_registry_rebuilds_from_visible_symbols() {
2425 let mut registry = SymbolAssetRegistry::new();
2426 let mut engine = SymbolPlacementEngine::new();
2427 let placed = engine.place_candidates(
2428 &[SymbolCandidate {
2429 id: "icon".into(),
2430 layer_id: Some("symbols".into()),
2431 source_id: Some("source".into()),
2432 source_layer: Some("poi".into()),
2433 source_tile: None,
2434 feature_id: "icon".into(),
2435 feature_index: 0,
2436 placement_group_id: "icon".into(),
2437 placement: SymbolPlacement::Point,
2438 anchor: GeoCoord::from_lat_lon(0.0, 0.0),
2439 text: Some("Hi".into()),
2440 icon_image: Some("marker".into()),
2441 font_stack: "Test Sans".into(),
2442 cross_tile_id: "icon".into(),
2443 rotation_rad: 0.0,
2444 size_px: 14.0,
2445 padding_px: 0.0,
2446 allow_overlap: true,
2447 ignore_placement: false,
2448 sort_key: None,
2449 radial_offset: None,
2450 variable_anchor_offsets: None,
2451 text_max_width: None,
2452 text_line_height: None,
2453 text_letter_spacing: None,
2454 icon_text_fit: SymbolIconTextFit::None,
2455 icon_text_fit_padding: [0.0, 0.0, 0.0, 0.0],
2456 anchors: vec![SymbolAnchor::Center],
2457 writing_mode: SymbolWritingMode::Horizontal,
2458 offset_px: [0.0, 0.0],
2459 fill_color: [1.0, 1.0, 1.0, 1.0],
2460 halo_color: [0.0, 0.0, 0.0, 1.0],
2461 }],
2462 CameraProjection::WebMercator,
2463 1.0,
2464 0.5,
2465 None,
2466 );
2467 registry.rebuild_from_symbols(&placed);
2468 assert!(registry.glyphs().len() >= 2);
2469 assert_eq!(
2470 registry.images().referenced().collect::<Vec<_>>(),
2471 vec!["marker"]
2472 );
2473 }
2474
2475 #[test]
2476 fn placed_symbols_generate_render_mesh() {
2477 let mut engine = SymbolPlacementEngine::new();
2478 let placed = engine.place_candidates(
2479 &[candidate("a", 0.0, "Alpha")],
2480 CameraProjection::WebMercator,
2481 1.0,
2482 1.0,
2483 None,
2484 );
2485 let mesh = symbol_mesh_from_placed_symbols(&placed, CameraProjection::WebMercator, 1.0);
2486 assert_eq!(mesh.vertex_count(), 8);
2487 assert_eq!(mesh.index_count(), 12);
2488 }
2489
2490 #[test]
2491 fn rotated_collision_box_expands_for_diagonal_symbols() {
2492 let bbox = rotated_collision_box(0.0, 0.0, 10.0, 2.0, std::f32::consts::FRAC_PI_4);
2493 let width = bbox.max[0] - bbox.min[0];
2497 let height = bbox.max[1] - bbox.min[1];
2498 assert!(
2499 width > 4.0,
2500 "rotated AABB width {width} should exceed the unrotated height"
2501 );
2502 assert!(
2503 height > 4.0,
2504 "rotated AABB height {height} should exceed the unrotated height"
2505 );
2506 assert!(
2508 (width - height).abs() < 1.0,
2509 "45 deg rotation should produce a near-square AABB"
2510 );
2511 }
2512
2513 #[test]
2514 fn line_candidates_use_multiple_collision_boxes() {
2515 let mut line = candidate("line", 0.0, "Long river label");
2516 line.placement = SymbolPlacement::Line;
2517 line.rotation_rad = std::f32::consts::FRAC_PI_4;
2518
2519 let boxes = candidate_collision_boxes(
2520 &line,
2521 CameraProjection::WebMercator,
2522 SymbolAnchor::Center,
2523 2.0,
2524 );
2525 assert!(boxes.len() > 1);
2526 }
2527
2528 #[test]
2529 fn rotated_line_candidates_can_coexist_when_segment_boxes_no_longer_overlap() {
2530 let mut a = candidate("a", 0.0, "Alpha");
2531 a.placement = SymbolPlacement::Line;
2532 a.rotation_rad = std::f32::consts::FRAC_PI_2;
2533
2534 let mut b = candidate("b", 0.0005, "Beta");
2535 b.placement = SymbolPlacement::Line;
2536 b.rotation_rad = std::f32::consts::FRAC_PI_2;
2537
2538 let mut engine = SymbolPlacementEngine::new();
2539 let placed =
2540 engine.place_candidates(&[a, b], CameraProjection::WebMercator, 2.0, 1.0, None);
2541 assert_eq!(placed.iter().filter(|symbol| symbol.visible).count(), 2);
2542 }
2543
2544 #[test]
2545 fn equirectangular_symbol_anchor_differs_from_mercator() {
2546 let mut engine = SymbolPlacementEngine::new();
2547 let mut merc_candidate = candidate("a", 10.0, "Alpha");
2548 merc_candidate.anchor = GeoCoord::from_lat_lon(45.0, 10.0);
2549 let mut eq_candidate = candidate("b", 10.0, "Alpha");
2550 eq_candidate.anchor = GeoCoord::from_lat_lon(45.0, 10.0);
2551 let merc = engine.place_candidates(
2552 &[merc_candidate],
2553 CameraProjection::WebMercator,
2554 1.0,
2555 1.0,
2556 None,
2557 );
2558 let eq = engine.place_candidates(
2559 &[eq_candidate],
2560 CameraProjection::Equirectangular,
2561 1.0,
2562 1.0,
2563 None,
2564 );
2565 assert_eq!(merc.len(), 1);
2566 assert_eq!(eq.len(), 1);
2567 assert!((merc[0].world_anchor[1] - eq[0].world_anchor[1]).abs() > 1.0);
2568 }
2569
2570 #[test]
2573 fn sdf_single_pixel_inside_produces_centered_gradient() {
2574 let alpha = vec![255u8];
2577 let sdf = binary_to_sdf(&alpha, 1, 1, 3);
2578 let w = 1 + 2 * 3; assert_eq!(sdf.len(), w * w);
2580 let center = sdf[3 * w + 3];
2582 assert!(
2583 center > 140,
2584 "center SDF value {center} should be inside (>140)"
2585 );
2586 let corner = sdf[0];
2588 assert!(
2589 corner < 100,
2590 "corner SDF value {corner} should be outside (<100)"
2591 );
2592 }
2593
2594 #[test]
2595 fn sdf_empty_bitmap_is_all_outside() {
2596 let alpha = vec![0u8; 4];
2598 let sdf = binary_to_sdf(&alpha, 2, 2, 2);
2599 for &v in &sdf {
2600 assert!(v <= 128, "SDF value {v} should be ≤128 for all-outside");
2601 }
2602 }
2603
2604 #[test]
2605 fn sdf_full_bitmap_is_all_inside() {
2606 let alpha = vec![255u8; 4];
2608 let sdf = binary_to_sdf(&alpha, 2, 2, 2);
2609 let w = 2 + 2 * 2;
2610 let interior = sdf[(2) * w + (2)]; assert!(interior > 128, "SDF interior {interior} should be >128");
2613 }
2614
2615 #[test]
2616 fn sdf_adds_padding_to_dimensions() {
2617 let alpha = vec![255u8; 6]; let sdf = binary_to_sdf(&alpha, 3, 2, 3);
2619 assert_eq!(sdf.len(), (3 + 6) * (2 + 6)); }
2621
2622 #[test]
2623 fn sdf_zero_size_returns_empty() {
2624 assert!(binary_to_sdf(&[], 0, 0, 3).is_empty());
2625 }
2626
2627 #[test]
2628 fn glyph_atlas_sdf_entries_include_padding() {
2629 let mut atlas = GlyphAtlas::new();
2632 atlas.request_text("Test Sans", "A");
2633 atlas.load_requested(&ProceduralGlyphProvider::new());
2634 let entry = atlas.get("Test Sans", 'A').expect("glyph A");
2635 assert_eq!(entry.size[0], 8 + 2 * SDF_BUFFER);
2637 assert_eq!(entry.size[1], 12 + 2 * SDF_BUFFER);
2638 }
2639
2640 #[test]
2641 fn glyph_atlas_sdf_alpha_contains_gradient_values() {
2642 let mut atlas = GlyphAtlas::new();
2644 atlas.request_text("Test Sans", "X");
2645 atlas.load_requested(&ProceduralGlyphProvider::new());
2646 let has_intermediate = atlas.alpha().iter().any(|&v| v > 10 && v < 245);
2647 assert!(
2648 has_intermediate,
2649 "SDF atlas should contain gradient values between 0 and 255"
2650 );
2651 }
2652
2653 #[test]
2654 fn glyph_atlas_render_em_px_from_procedural() {
2655 let mut atlas = GlyphAtlas::new();
2656 atlas.request_text("Test Sans", "A");
2657 atlas.load_requested(&ProceduralGlyphProvider::new());
2658 assert_eq!(atlas.render_em_px(), 12.0);
2659 }
2660
2661 #[test]
2662 fn edt_1d_seeds_remain_zero() {
2663 let mut f = vec![0.0, 1e10, 1e10, 0.0, 1e10];
2664 edt_1d(&mut f);
2665 assert_eq!(f[0], 0.0);
2666 assert_eq!(f[3], 0.0);
2667 assert!(f[1] > 0.0);
2668 assert!(f[2] > 0.0);
2669 }
2670
2671 #[test]
2672 fn edt_1d_distances_increase_from_seed() {
2673 let mut f = vec![0.0, 1e10, 1e10, 1e10, 1e10];
2674 edt_1d(&mut f);
2675 assert!((f[0] - 0.0).abs() < 0.01);
2677 assert!((f[1] - 1.0).abs() < 0.01);
2678 assert!((f[2] - 4.0).abs() < 0.01);
2679 assert!((f[3] - 9.0).abs() < 0.01);
2680 assert!((f[4] - 16.0).abs() < 0.01);
2681 }
2682}