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)]
74pub enum SymbolWritingMode {
75 Horizontal,
77 Vertical,
79}
80
81impl Default for SymbolWritingMode {
82 fn default() -> Self {
83 Self::Horizontal
84 }
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
89pub enum SymbolTextJustify {
90 Auto,
92 Left,
94 Center,
96 Right,
98}
99
100impl Default for SymbolTextJustify {
101 fn default() -> Self {
102 Self::Auto
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
108pub enum SymbolTextTransform {
109 None,
111 Uppercase,
113 Lowercase,
115}
116
117impl Default for SymbolTextTransform {
118 fn default() -> Self {
119 Self::None
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
125pub enum SymbolIconTextFit {
126 None,
128 Width,
130 Height,
132 Both,
134}
135
136impl Default for SymbolIconTextFit {
137 fn default() -> Self {
138 Self::None
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
150pub enum SymbolPlacement {
151 Point,
153 Line,
155}
156
157impl Default for SymbolPlacement {
158 fn default() -> Self {
159 Self::Point
160 }
161}
162
163#[derive(Debug, Clone, PartialEq)]
165pub struct GlyphRaster {
166 pub width: u16,
168 pub height: u16,
170 pub advance_x: f32,
172 pub bearing_x: i16,
174 pub bearing_y: i16,
176 pub alpha: Vec<u8>,
178}
179
180impl GlyphRaster {
181 pub fn new(
183 width: u16,
184 height: u16,
185 advance_x: f32,
186 bearing_x: i16,
187 bearing_y: i16,
188 alpha: Vec<u8>,
189 ) -> Self {
190 Self {
191 width,
192 height,
193 advance_x,
194 bearing_x,
195 bearing_y,
196 alpha,
197 }
198 }
199}
200
201#[derive(Debug, Clone, PartialEq)]
203pub struct GlyphAtlasEntry {
204 pub key: GlyphKey,
206 pub origin: [u16; 2],
208 pub size: [u16; 2],
210 pub advance_x: f32,
212 pub bearing_x: i16,
214 pub bearing_y: i16,
216}
217
218pub trait GlyphProvider: Send + Sync {
220 fn load_glyph(&self, font_stack: &str, codepoint: char) -> Option<GlyphRaster>;
222
223 fn render_em_pixels(&self) -> f32 {
229 24.0
230 }
231}
232
233#[derive(Debug, Clone, Default)]
235pub struct ProceduralGlyphProvider;
236
237impl ProceduralGlyphProvider {
238 pub fn new() -> Self {
240 Self
241 }
242}
243
244impl GlyphProvider for ProceduralGlyphProvider {
245 fn load_glyph(&self, _font_stack: &str, codepoint: char) -> Option<GlyphRaster> {
246 let width = 8u16;
247 let height = 12u16;
248 let mut alpha = vec![0u8; width as usize * height as usize];
249 let seed = codepoint as u32;
250 for y in 0..height as usize {
251 for x in 0..width as usize {
252 let border = x == 0 || y == 0 || x + 1 == width as usize || y + 1 == height as usize;
253 let bits = ((seed >> ((x + y) % 8)) & 1) != 0;
254 let horizontal = y == 3 || y == 6 || y == 9;
255 let vertical = x == 2 || x == 5;
256 alpha[y * width as usize + x] = if border || (bits && (horizontal || vertical)) {
257 255
258 } else {
259 0
260 };
261 }
262 }
263 Some(GlyphRaster::new(width, height, width as f32 + 1.0, 0, height as i16, alpha))
264 }
265
266 fn render_em_pixels(&self) -> f32 {
267 12.0
268 }
269}
270
271const SDF_BUFFER: u16 = 3;
277
278#[derive(Debug, Clone)]
280pub struct GlyphAtlas {
281 requested: BTreeSet<GlyphKey>,
282 entries: HashMap<GlyphKey, GlyphAtlasEntry>,
283 alpha: Vec<u8>,
284 dimensions: [u16; 2],
285 render_em_px: f32,
286}
287
288impl Default for GlyphAtlas {
289 fn default() -> Self {
290 Self {
291 requested: BTreeSet::new(),
292 entries: HashMap::new(),
293 alpha: Vec::new(),
294 dimensions: [0, 0],
295 render_em_px: 24.0,
296 }
297 }
298}
299
300impl GlyphAtlas {
301 pub fn new() -> Self {
303 Self::default()
304 }
305
306 pub fn request_text(&mut self, font_stack: &str, text: &str) {
308 for codepoint in text.chars() {
309 self.requested.insert(GlyphKey {
310 font_stack: font_stack.to_owned(),
311 codepoint,
312 });
313 }
314 }
315
316 pub fn requested(&self) -> impl Iterator<Item = &GlyphKey> {
318 self.requested.iter()
319 }
320
321 pub fn len(&self) -> usize {
323 self.requested.len()
324 }
325
326 pub fn is_empty(&self) -> bool {
328 self.requested.is_empty()
329 }
330
331 pub fn entries(&self) -> impl Iterator<Item = &GlyphAtlasEntry> {
333 self.entries.values()
334 }
335
336 pub fn get(&self, font_stack: &str, codepoint: char) -> Option<&GlyphAtlasEntry> {
338 self.entries.get(&GlyphKey {
339 font_stack: font_stack.to_owned(),
340 codepoint,
341 })
342 }
343
344 pub fn alpha(&self) -> &[u8] {
346 &self.alpha
347 }
348
349 pub fn dimensions(&self) -> [u16; 2] {
351 self.dimensions
352 }
353
354 pub fn render_em_px(&self) -> f32 {
357 self.render_em_px
358 }
359
360 pub fn load_requested(&mut self, provider: &dyn GlyphProvider) {
366 self.entries.clear();
367 self.alpha.clear();
368 self.dimensions = [0, 0];
369 self.render_em_px = provider.render_em_pixels();
370
371 let mut rasters: Vec<(GlyphKey, GlyphRaster)> = Vec::new();
372 for key in &self.requested {
373 if let Some(raster) = provider.load_glyph(&key.font_stack, key.codepoint) {
374 rasters.push((key.clone(), raster));
375 }
376 }
377 if rasters.is_empty() {
378 return;
379 }
380
381 let buf = SDF_BUFFER;
383 let rasters: Vec<(GlyphKey, GlyphRaster)> = rasters
384 .into_iter()
385 .map(|(key, raster)| {
386 if raster.width == 0 || raster.height == 0 {
387 return (key, raster); }
389 let sdf_alpha = binary_to_sdf(
390 &raster.alpha,
391 raster.width as usize,
392 raster.height as usize,
393 buf as usize,
394 );
395 (
396 key,
397 GlyphRaster::new(
398 raster.width + buf * 2,
399 raster.height + buf * 2,
400 raster.advance_x,
401 raster.bearing_x - buf as i16,
402 raster.bearing_y + buf as i16,
403 sdf_alpha,
404 ),
405 )
406 })
407 .collect();
408
409 let padding = 1u16;
410 let atlas_width = rasters
411 .iter()
412 .map(|(_, raster)| raster.width + padding)
413 .sum::<u16>()
414 .max(1);
415 let atlas_height = rasters
416 .iter()
417 .map(|(_, raster)| raster.height)
418 .max()
419 .unwrap_or(0)
420 + padding * 2;
421
422 self.dimensions = [atlas_width, atlas_height.max(1)];
423 self.alpha = vec![0; atlas_width as usize * self.dimensions[1] as usize];
424
425 let mut cursor_x = padding;
426 for (key, raster) in rasters {
427 let atlas_key = key.clone();
428 let origin = [cursor_x, padding];
429 blit_alpha(
430 &mut self.alpha,
431 self.dimensions[0] as usize,
432 origin,
433 raster.width as usize,
434 raster.height as usize,
435 &raster.alpha,
436 );
437 self.entries.insert(
438 atlas_key,
439 GlyphAtlasEntry {
440 key,
441 origin,
442 size: [raster.width, raster.height],
443 advance_x: raster.advance_x,
444 bearing_x: raster.bearing_x,
445 bearing_y: raster.bearing_y,
446 },
447 );
448 cursor_x += raster.width + padding;
449 }
450 }
451}
452
453#[derive(Debug, Clone, PartialEq, Eq)]
455pub struct SpriteImage {
456 pub id: String,
458 pub origin: [u32; 2],
460 pub pixel_size: [u32; 2],
462}
463
464impl SpriteImage {
465 pub fn new(id: impl Into<String>, pixel_size: [u32; 2]) -> Self {
467 Self {
468 id: id.into(),
469 origin: [0, 0],
470 pixel_size,
471 }
472 }
473
474 pub fn with_origin(mut self, origin: [u32; 2]) -> Self {
476 self.origin = origin;
477 self
478 }
479}
480
481#[derive(Debug, Clone, Default)]
483pub struct SpriteSheet {
484 images: HashMap<String, SpriteImage>,
485}
486
487impl SpriteSheet {
488 pub fn new() -> Self {
490 Self::default()
491 }
492
493 pub fn register(&mut self, image: SpriteImage) {
495 self.images.insert(image.id.clone(), image);
496 }
497
498 pub fn get(&self, id: &str) -> Option<&SpriteImage> {
500 self.images.get(id)
501 }
502
503 pub fn iter(&self) -> impl Iterator<Item = &SpriteImage> {
505 self.images.values()
506 }
507
508 #[cfg(feature = "style-json")]
510 pub fn from_index_json(json: &str) -> Result<Self, SpriteSheetParseError> {
511 use serde::Deserialize;
512
513 #[derive(Deserialize)]
514 struct SpriteIndexEntry {
515 x: u32,
516 y: u32,
517 width: u32,
518 height: u32,
519 }
520
521 let index: HashMap<String, SpriteIndexEntry> = serde_json::from_str(json)
522 .map_err(SpriteSheetParseError::Json)?;
523 let mut sheet = SpriteSheet::new();
524 for (id, entry) in index {
525 sheet.register(
526 SpriteImage::new(id, [entry.width, entry.height]).with_origin([entry.x, entry.y]),
527 );
528 }
529 Ok(sheet)
530 }
531}
532
533#[cfg(feature = "style-json")]
535#[derive(Debug)]
536pub enum SpriteSheetParseError {
537 Json(serde_json::Error),
539}
540
541#[cfg(feature = "style-json")]
542impl std::fmt::Display for SpriteSheetParseError {
543 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
544 match self {
545 SpriteSheetParseError::Json(err) => write!(f, "sprite sheet parse error: {err}"),
546 }
547 }
548}
549
550#[cfg(feature = "style-json")]
551impl std::error::Error for SpriteSheetParseError {}
552
553#[derive(Debug, Clone, Default)]
555pub struct ImageManager {
556 sprites: SpriteSheet,
557 images: HashMap<String, SpriteImage>,
558 referenced: BTreeSet<String>,
559}
560
561impl ImageManager {
562 pub fn new() -> Self {
564 Self::default()
565 }
566
567 pub fn register_sprite(&mut self, image: SpriteImage) {
569 self.sprites.register(image);
570 }
571
572 pub fn register_image(&mut self, image: SpriteImage) {
574 self.images.insert(image.id.clone(), image);
575 }
576
577 #[cfg(feature = "style-json")]
579 pub fn load_sprite_sheet_index_json(&mut self, json: &str) -> Result<(), SpriteSheetParseError> {
580 self.sprites = SpriteSheet::from_index_json(json)?;
581 Ok(())
582 }
583
584 pub fn request(&mut self, id: &str) {
586 self.referenced.insert(id.to_owned());
587 }
588
589 pub fn contains(&self, id: &str) -> bool {
591 self.images.contains_key(id) || self.sprites.get(id).is_some()
592 }
593
594 pub fn referenced(&self) -> impl Iterator<Item = &str> {
596 self.referenced.iter().map(String::as_str)
597 }
598
599 fn clear_requests(&mut self) {
600 self.referenced.clear();
601 }
602}
603
604#[derive(Debug, Clone, Default, PartialEq, Eq)]
606pub struct SymbolAssetDependencies {
607 pub glyphs: BTreeSet<GlyphKey>,
609 pub images: BTreeSet<String>,
611}
612
613#[derive(Debug, Clone, Default)]
615pub struct SymbolAssetRegistry {
616 glyphs: GlyphAtlas,
617 images: ImageManager,
618}
619
620impl SymbolAssetRegistry {
621 pub fn new() -> Self {
623 Self::default()
624 }
625
626 pub fn glyphs(&self) -> &GlyphAtlas {
628 &self.glyphs
629 }
630
631 pub fn images(&self) -> &ImageManager {
633 &self.images
634 }
635
636 pub fn images_mut(&mut self) -> &mut ImageManager {
638 &mut self.images
639 }
640
641 pub fn rebuild_from_symbols(&mut self, symbols: &[PlacedSymbol]) {
643 self.glyphs = GlyphAtlas::default();
644 self.images.clear_requests();
645 for symbol in symbols.iter().filter(|symbol| symbol.visible) {
646 if let Some(text) = &symbol.text {
647 self.glyphs.request_text(&symbol.font_stack, text);
648 }
649 if let Some(icon) = &symbol.icon_image {
650 self.images.request(icon);
651 }
652 }
653 }
654}
655
656#[derive(Debug, Clone)]
658pub struct SymbolCandidate {
659 pub id: String,
661 pub layer_id: Option<String>,
663 pub source_id: Option<String>,
665 pub source_layer: Option<String>,
667 pub source_tile: Option<TileId>,
669 pub feature_id: String,
671 pub feature_index: usize,
673 pub placement_group_id: String,
680 pub placement: SymbolPlacement,
682 pub anchor: GeoCoord,
684 pub text: Option<String>,
686 pub icon_image: Option<String>,
688 pub font_stack: String,
690 pub cross_tile_id: String,
692 pub rotation_rad: f32,
698 pub size_px: f32,
700 pub padding_px: f32,
702 pub allow_overlap: bool,
704 pub ignore_placement: bool,
706 pub sort_key: Option<f32>,
711 pub radial_offset: Option<f32>,
713 pub variable_anchor_offsets: Option<Vec<(SymbolAnchor, [f32; 2])>>,
715 pub text_max_width: Option<f32>,
717 pub text_line_height: Option<f32>,
719 pub text_letter_spacing: Option<f32>,
721 pub icon_text_fit: SymbolIconTextFit,
723 pub icon_text_fit_padding: [f32; 4],
725 pub anchors: Vec<SymbolAnchor>,
727 pub writing_mode: SymbolWritingMode,
729 pub offset_px: [f32; 2],
731 pub fill_color: [f32; 4],
733 pub halo_color: [f32; 4],
735}
736
737impl SymbolCandidate {
738 pub fn dependencies(&self) -> SymbolAssetDependencies {
740 let mut deps = SymbolAssetDependencies::default();
741 if let Some(text) = &self.text {
742 for codepoint in text.chars() {
743 deps.glyphs.insert(GlyphKey {
744 font_stack: self.font_stack.clone(),
745 codepoint,
746 });
747 }
748 }
749 if let Some(icon) = &self.icon_image {
750 deps.images.insert(icon.clone());
751 }
752 deps
753 }
754
755 fn dedupe_key(&self, meters_per_pixel: f64) -> String {
756 if !self.cross_tile_id.is_empty() {
757 return self.cross_tile_id.clone();
758 }
759 cross_tile_id_for_symbol(self.text.as_deref(), self.icon_image.as_deref(), &self.anchor, meters_per_pixel)
760 }
761}
762
763#[derive(Debug, Clone, PartialEq)]
765pub struct SymbolCollisionBox {
766 pub min: [f64; 2],
768 pub max: [f64; 2],
770}
771
772impl SymbolCollisionBox {
773 pub fn intersects(&self, other: &Self) -> bool {
775 !(self.max[0] <= other.min[0]
776 || self.min[0] >= other.max[0]
777 || self.max[1] <= other.min[1]
778 || self.min[1] >= other.max[1])
779 }
780}
781
782#[derive(Debug, Clone, PartialEq)]
787pub struct GlyphQuad {
788 pub codepoint: char,
790 pub x: f32,
792 pub y: f32,
794}
795
796#[derive(Debug, Clone)]
798pub struct PlacedSymbol {
799 pub id: String,
801 pub layer_id: Option<String>,
803 pub source_id: Option<String>,
805 pub source_layer: Option<String>,
807 pub source_tile: Option<TileId>,
809 pub feature_id: String,
811 pub feature_index: usize,
813 pub placement: SymbolPlacement,
815 pub anchor: GeoCoord,
817 pub world_anchor: [f64; 3],
819 pub text: Option<String>,
821 pub icon_image: Option<String>,
823 pub font_stack: String,
825 pub cross_tile_id: String,
827 pub rotation_rad: f32,
829 pub collision_box: SymbolCollisionBox,
831 pub anchor_mode: SymbolAnchor,
833 pub writing_mode: SymbolWritingMode,
835 pub offset_px: [f32; 2],
837 pub radial_offset: Option<f32>,
839 pub text_max_width: Option<f32>,
841 pub text_line_height: Option<f32>,
843 pub text_letter_spacing: Option<f32>,
845 pub icon_text_fit: SymbolIconTextFit,
847 pub icon_text_fit_padding: [f32; 4],
849 pub size_px: f32,
851 pub fill_color: [f32; 4],
853 pub halo_color: [f32; 4],
855 pub opacity: f32,
857 pub visible: bool,
859 pub glyph_quads: Vec<GlyphQuad>,
865}
866
867impl PlacedSymbol {
868 pub fn dependencies(&self) -> SymbolAssetDependencies {
870 SymbolCandidate {
871 id: self.id.clone(),
872 layer_id: self.layer_id.clone(),
873 source_id: self.source_id.clone(),
874 source_layer: self.source_layer.clone(),
875 source_tile: self.source_tile,
876 feature_id: self.feature_id.clone(),
877 feature_index: self.feature_index,
878 placement_group_id: self.id.clone(),
879 placement: self.placement,
880 anchor: self.anchor,
881 text: self.text.clone(),
882 icon_image: self.icon_image.clone(),
883 font_stack: self.font_stack.clone(),
884 cross_tile_id: self.cross_tile_id.clone(),
885 rotation_rad: self.rotation_rad,
886 size_px: 0.0,
887 padding_px: 0.0,
888 allow_overlap: true,
889 ignore_placement: false,
890 sort_key: None,
891 radial_offset: self.radial_offset,
892 variable_anchor_offsets: None,
893 text_max_width: self.text_max_width,
894 text_line_height: self.text_line_height,
895 text_letter_spacing: self.text_letter_spacing,
896 icon_text_fit: self.icon_text_fit,
897 icon_text_fit_padding: self.icon_text_fit_padding,
898 anchors: vec![self.anchor_mode],
899 writing_mode: self.writing_mode,
900 offset_px: self.offset_px,
901 fill_color: self.fill_color,
902 halo_color: self.halo_color,
903 }
904 .dependencies()
905 }
906}
907
908#[derive(Debug, Clone)]
910pub struct SymbolPlacementConfig {
911 pub fade_in_per_second: f32,
913 pub fade_out_per_second: f32,
915 pub viewport_padding_factor: f64,
917}
918
919impl Default for SymbolPlacementConfig {
920 fn default() -> Self {
921 Self {
922 fade_in_per_second: 6.0,
923 fade_out_per_second: 8.0,
924 viewport_padding_factor: 2.0,
925 }
926 }
927}
928
929#[derive(Debug, Clone, Default)]
931pub struct SymbolPlacementEngine {
932 pub config: SymbolPlacementConfig,
934 previous_opacity: HashMap<String, f32>,
935 previous_anchor: HashMap<String, SymbolAnchor>,
936 pub cross_tile_index: cross_tile_index::CrossTileSymbolIndex,
938}
939
940impl SymbolPlacementEngine {
941 pub fn new() -> Self {
943 Self::default()
944 }
945
946 pub fn remove_tile(&mut self, tile_id: &TileId) {
951 self.cross_tile_index.remove_tile(tile_id);
952 }
953
954 pub fn place_candidates(
956 &mut self,
957 candidates: &[SymbolCandidate],
958 projection: CameraProjection,
959 meters_per_pixel: f64,
960 dt_seconds: f64,
961 viewport_bounds: Option<&WorldBounds>,
962 ) -> Vec<PlacedSymbol> {
963 let dt = dt_seconds.max(0.0) as f32;
964 let grouped_candidates = Self::group_symbol_candidates(candidates, meters_per_pixel);
965 let mut result = Vec::new();
966 let mut accepted_boxes = Vec::new();
967 let mut seen_ids = HashSet::new();
968 let mut dedupe = HashSet::new();
969
970 for (placement_id, variants) in grouped_candidates {
971 let Some(primary_candidate) = variants.first() else {
972 continue;
973 };
974 seen_ids.insert(placement_id.clone());
975 if !dedupe.insert(placement_id.clone()) {
976 continue;
977 }
978
979 let anchors = ordered_candidate_anchors(
980 primary_candidate,
981 self.previous_anchor.get(&placement_id).copied(),
982 );
983 let mut chosen = None;
984 for candidate in &variants {
985 if candidate.text.is_none() && candidate.icon_image.is_none() {
986 continue;
987 }
988 for anchor in anchors.iter().copied() {
989 let collision_boxes =
990 candidate_collision_boxes(candidate, projection, anchor, meters_per_pixel);
991 if collision_boxes.is_empty()
992 || !collision_boxes.iter().any(|bbox| {
993 viewport_contains_box(
994 viewport_bounds,
995 bbox,
996 self.config.viewport_padding_factor,
997 )
998 })
999 {
1000 continue;
1001 }
1002 let collides = !candidate.allow_overlap
1003 && accepted_boxes
1004 .iter()
1005 .any(|other| collision_boxes.iter().any(|bbox| bbox.intersects(other)));
1006 if !collides {
1007 chosen = Some((candidate, anchor, collision_boxes));
1008 break;
1009 }
1010 }
1011 if chosen.is_some() {
1012 break;
1013 }
1014 }
1015
1016 let prev = self.previous_opacity.get(&placement_id).copied().unwrap_or(0.0);
1017 let opacity = if chosen.is_none() {
1018 (prev - self.config.fade_out_per_second * dt).max(0.0)
1019 } else {
1020 (prev + self.config.fade_in_per_second * dt).clamp(0.0, 1.0)
1021 };
1022
1023 self.previous_opacity.insert(placement_id.clone(), opacity);
1024 if opacity <= 0.0 {
1025 continue;
1026 }
1027
1028 let (selected_candidate, anchor_mode, collision_box, visible) = if let Some((selected_candidate, anchor_mode, collision_boxes)) = chosen {
1029 if !selected_candidate.ignore_placement {
1030 accepted_boxes.extend(collision_boxes.iter().cloned());
1031 }
1032 self.previous_anchor.insert(placement_id.clone(), anchor_mode);
1033 (
1034 selected_candidate,
1035 anchor_mode,
1036 merge_collision_boxes(&collision_boxes),
1037 true,
1038 )
1039 } else {
1040 let fallback_anchor = self
1041 .previous_anchor
1042 .get(&placement_id)
1043 .copied()
1044 .unwrap_or_else(|| {
1045 primary_candidate
1046 .anchors
1047 .first()
1048 .copied()
1049 .unwrap_or(SymbolAnchor::Center)
1050 });
1051 let collision_boxes = candidate_collision_boxes(
1052 primary_candidate,
1053 projection,
1054 fallback_anchor,
1055 meters_per_pixel,
1056 );
1057 (
1058 primary_candidate,
1059 fallback_anchor,
1060 merge_collision_boxes(&collision_boxes),
1061 false,
1062 )
1063 };
1064
1065 let world = projection.project(&selected_candidate.anchor);
1066 result.push(PlacedSymbol {
1067 id: selected_candidate.id.clone(),
1068 layer_id: selected_candidate.layer_id.clone(),
1069 source_id: selected_candidate.source_id.clone(),
1070 source_layer: selected_candidate.source_layer.clone(),
1071 source_tile: selected_candidate.source_tile,
1072 feature_id: selected_candidate.feature_id.clone(),
1073 feature_index: selected_candidate.feature_index,
1074 placement: selected_candidate.placement,
1075 anchor: selected_candidate.anchor,
1076 world_anchor: [world.position.x, world.position.y, world.position.z],
1077 text: selected_candidate.text.clone(),
1078 icon_image: selected_candidate.icon_image.clone(),
1079 font_stack: selected_candidate.font_stack.clone(),
1080 cross_tile_id: placement_id.clone(),
1081 rotation_rad: selected_candidate.rotation_rad,
1082 collision_box,
1083 anchor_mode,
1084 writing_mode: selected_candidate.writing_mode,
1085 offset_px: resolve_symbol_offset(
1086 anchor_mode,
1087 selected_candidate.offset_px,
1088 selected_candidate.radial_offset,
1089 selected_candidate.variable_anchor_offsets.as_deref(),
1090 selected_candidate.size_px,
1091 ),
1092 radial_offset: None,
1093 text_max_width: selected_candidate.text_max_width,
1094 text_line_height: selected_candidate.text_line_height,
1095 text_letter_spacing: selected_candidate.text_letter_spacing,
1096 icon_text_fit: selected_candidate.icon_text_fit,
1097 icon_text_fit_padding: selected_candidate.icon_text_fit_padding,
1098 size_px: selected_candidate.size_px,
1099 fill_color: selected_candidate.fill_color,
1100 halo_color: selected_candidate.halo_color,
1101 opacity,
1102 visible,
1103 glyph_quads: Vec::new(),
1104 });
1105 }
1106
1107 self.previous_opacity
1108 .retain(|id, opacity| seen_ids.contains(id) && *opacity > 0.0);
1109 self.previous_anchor.retain(|id, _| seen_ids.contains(id));
1110 result
1111 }
1112
1113fn group_symbol_candidates<'a>(
1121 candidates: &'a [SymbolCandidate],
1122 meters_per_pixel: f64,
1123) -> Vec<(String, Vec<&'a SymbolCandidate>)> {
1124 let mut grouped: Vec<(String, Vec<&'a SymbolCandidate>)> = Vec::new();
1125 let mut group_indexes: HashMap<String, usize> = HashMap::new();
1126
1127 for candidate in candidates {
1128 let placement_id = candidate.dedupe_key(meters_per_pixel);
1129 let group_key = format!("{}|{}", placement_id, candidate.placement_group_id);
1130 if let Some(index) = group_indexes.get(&group_key).copied() {
1131 grouped[index].1.push(candidate);
1132 } else {
1133 group_indexes.insert(group_key, grouped.len());
1134 grouped.push((placement_id, vec![candidate]));
1135 }
1136 }
1137
1138 if grouped
1144 .iter()
1145 .any(|(_, variants)| variants.first().and_then(|candidate| candidate.sort_key).is_some())
1146 {
1147 grouped.sort_by(|(_, a_variants), (_, b_variants)| {
1148 let a_key = a_variants.first().and_then(|candidate| candidate.sort_key).unwrap_or(0.0);
1149 let b_key = b_variants.first().and_then(|candidate| candidate.sort_key).unwrap_or(0.0);
1150 a_key
1151 .partial_cmp(&b_key)
1152 .unwrap_or(std::cmp::Ordering::Equal)
1153 });
1154 }
1155
1156 grouped
1157}
1158}
1159
1160pub fn symbol_mesh_from_placed_symbols(
1162 symbols: &[PlacedSymbol],
1163 projection: CameraProjection,
1164 meters_per_pixel: f64,
1165) -> crate::layers::VectorMeshData {
1166 let mut mesh = crate::layers::VectorMeshData::default();
1167 for symbol in symbols.iter().filter(|symbol| symbol.visible && symbol.opacity > 0.0) {
1168 let [half_w, half_h] = symbol_half_extents(
1169 symbol.size_px,
1170 symbol.text.as_deref(),
1171 symbol.icon_image.as_deref(),
1172 symbol.writing_mode,
1173 symbol.placement,
1174 symbol.text_max_width,
1175 symbol.text_line_height,
1176 symbol.text_letter_spacing,
1177 symbol.icon_text_fit,
1178 symbol.icon_text_fit_padding,
1179 symbol.offset_px,
1180 1.0,
1181 );
1182 append_symbol_quad(&mut mesh, symbol, projection, half_w * meters_per_pixel * 1.35, half_h * meters_per_pixel * 1.35, symbol.halo_color, symbol.opacity, meters_per_pixel);
1183 append_symbol_quad(&mut mesh, symbol, projection, half_w * meters_per_pixel, half_h * meters_per_pixel, symbol.fill_color, symbol.opacity, meters_per_pixel);
1184 }
1185 mesh
1186}
1187
1188fn anchor_align(anchor: SymbolAnchor) -> (f32, f32) {
1197 let h = match anchor {
1198 SymbolAnchor::Left | SymbolAnchor::TopLeft | SymbolAnchor::BottomLeft => 0.0,
1199 SymbolAnchor::Right | SymbolAnchor::TopRight | SymbolAnchor::BottomRight => 1.0,
1200 _ => 0.5,
1201 };
1202 let v = match anchor {
1203 SymbolAnchor::Top | SymbolAnchor::TopLeft | SymbolAnchor::TopRight => 0.0,
1204 SymbolAnchor::Bottom | SymbolAnchor::BottomLeft | SymbolAnchor::BottomRight => 1.0,
1205 _ => 0.5,
1206 };
1207 (h, v)
1208}
1209
1210pub fn layout_symbol_glyphs(symbols: &mut [PlacedSymbol], atlas: &GlyphAtlas) {
1222 let em = atlas.render_em_px();
1223 for symbol in symbols.iter_mut() {
1224 symbol.glyph_quads.clear();
1225 if !symbol.visible || symbol.opacity <= 0.0 {
1226 continue;
1227 }
1228 let text = match &symbol.text {
1229 Some(t) if !t.is_empty() => t.clone(),
1230 _ => continue,
1231 };
1232
1233 let scale = symbol.size_px / em.max(1.0);
1234 let letter_spacing = symbol.text_letter_spacing.unwrap_or(0.0) * symbol.size_px;
1235 let line_height = symbol.text_line_height.unwrap_or(1.2) * symbol.size_px;
1236
1237 let max_line_width_px = if symbol.placement == SymbolPlacement::Point {
1239 symbol.text_max_width.map(|w| w * symbol.size_px)
1240 } else {
1241 None
1242 };
1243
1244 let lines = break_text_simple(&text, atlas, &symbol.font_stack, scale, letter_spacing, max_line_width_px);
1245 if lines.is_empty() {
1246 continue;
1247 }
1248
1249 let line_widths: Vec<f32> = lines.iter().map(|glyphs| {
1251 glyphs.last().map(|(_, x, adv)| x + adv).unwrap_or(0.0)
1252 }).collect();
1253 let max_width = line_widths.iter().cloned().fold(0.0f32, f32::max);
1254 let total_height = lines.len() as f32 * line_height;
1255
1256 let (h_align, v_align) = anchor_align(symbol.anchor_mode);
1258
1259 let mut quads = Vec::new();
1260 for (line_idx, (glyphs, line_w)) in lines.iter().zip(line_widths.iter()).enumerate() {
1261 let line_shift_x = (max_width - line_w) * 0.5;
1263 let anchor_x = -max_width * h_align;
1265 let anchor_y = -total_height * v_align + line_idx as f32 * line_height;
1267
1268 for &(codepoint, glyph_x, _advance) in glyphs {
1269 quads.push(GlyphQuad {
1270 codepoint,
1271 x: anchor_x + line_shift_x + glyph_x,
1272 y: anchor_y,
1273 });
1274 }
1275 }
1276
1277 symbol.glyph_quads = quads;
1278 }
1279}
1280
1281fn break_text_simple(
1284 text: &str,
1285 atlas: &GlyphAtlas,
1286 font_stack: &str,
1287 scale: f32,
1288 letter_spacing: f32,
1289 max_width: Option<f32>,
1290) -> Vec<Vec<(char, f32, f32)>> {
1291 let chars: Vec<char> = text.chars().collect();
1292 if chars.is_empty() {
1293 return Vec::new();
1294 }
1295
1296 let mut advances: Vec<f32> = Vec::with_capacity(chars.len());
1298 for &ch in &chars {
1299 let adv = atlas.get(font_stack, ch)
1300 .map(|e| e.advance_x * scale)
1301 .unwrap_or(0.0);
1302 advances.push(adv);
1303 }
1304
1305 let mut lines: Vec<Vec<(char, f32, f32)>> = Vec::new();
1307 let mut current_line: Vec<(char, f32, f32)> = Vec::new();
1308 let mut cursor_x: f32 = 0.0;
1309
1310 for (i, &ch) in chars.iter().enumerate() {
1311 let adv = advances[i];
1312
1313 if ch == '\n' {
1315 lines.push(std::mem::take(&mut current_line));
1316 cursor_x = 0.0;
1317 continue;
1318 }
1319
1320 if let Some(max_w) = max_width {
1322 if ch == ' ' && cursor_x > 0.0 {
1323 let next_word_end = next_word_width(&chars, &advances, i + 1, letter_spacing);
1325 if cursor_x + adv + letter_spacing + next_word_end > max_w && !current_line.is_empty() {
1326 lines.push(std::mem::take(&mut current_line));
1327 cursor_x = 0.0;
1328 continue; }
1330 }
1331 }
1332
1333 if !current_line.is_empty() {
1334 cursor_x += letter_spacing;
1335 }
1336 current_line.push((ch, cursor_x, adv));
1337 cursor_x += adv;
1338 }
1339 if !current_line.is_empty() {
1340 lines.push(current_line);
1341 }
1342
1343 lines
1344}
1345
1346fn next_word_width(chars: &[char], advances: &[f32], start: usize, letter_spacing: f32) -> f32 {
1348 let mut w: f32 = 0.0;
1349 let mut first = true;
1350 for i in start..chars.len() {
1351 if chars[i] == ' ' || chars[i] == '\n' {
1352 break;
1353 }
1354 if !first {
1355 w += letter_spacing;
1356 }
1357 w += advances[i];
1358 first = false;
1359 }
1360 w
1361}
1362
1363#[cfg(feature = "text-shaping")]
1370pub fn layout_symbol_glyphs_shaped(
1371 symbols: &mut [PlacedSymbol],
1372 registry: &mut text_shaper::FontRegistry,
1373) {
1374 use text_shaper::{ShapeTextOptions, TextAnchor, TextJustify, shape_text, ONE_EM};
1375
1376 for symbol in symbols.iter_mut() {
1377 symbol.glyph_quads.clear();
1378 if !symbol.visible || symbol.opacity <= 0.0 {
1379 continue;
1380 }
1381 let text = match &symbol.text {
1382 Some(t) if !t.is_empty() => t.as_str(),
1383 _ => continue,
1384 };
1385
1386 let anchor = match symbol.anchor_mode {
1387 SymbolAnchor::Center => TextAnchor::Center,
1388 SymbolAnchor::Top => TextAnchor::Top,
1389 SymbolAnchor::Bottom => TextAnchor::Bottom,
1390 SymbolAnchor::Left => TextAnchor::Left,
1391 SymbolAnchor::Right => TextAnchor::Right,
1392 SymbolAnchor::TopLeft => TextAnchor::TopLeft,
1393 SymbolAnchor::TopRight => TextAnchor::TopRight,
1394 SymbolAnchor::BottomLeft => TextAnchor::BottomLeft,
1395 SymbolAnchor::BottomRight => TextAnchor::BottomRight,
1396 };
1397
1398 let options = ShapeTextOptions {
1399 font_stack: symbol.font_stack.clone(),
1400 max_width: if symbol.placement == SymbolPlacement::Point {
1401 symbol.text_max_width.or(Some(10.0))
1402 } else {
1403 None
1404 },
1405 line_height: symbol.text_line_height.unwrap_or(1.2),
1406 letter_spacing: symbol.text_letter_spacing.unwrap_or(0.0),
1407 justify: TextJustify::Center,
1408 anchor,
1409 writing_mode: symbol.writing_mode,
1410 text_transform: SymbolTextTransform::None,
1411 };
1412
1413 let shaped = match shape_text(text, registry, &options) {
1414 Some(s) => s,
1415 None => continue,
1416 };
1417
1418 let px_per_layout = symbol.size_px / ONE_EM;
1419 let mut quads = Vec::with_capacity(shaped.glyph_count());
1420 for line in &shaped.lines {
1421 for glyph in &line.glyphs {
1422 quads.push(GlyphQuad {
1423 codepoint: glyph.codepoint,
1424 x: glyph.x * px_per_layout,
1425 y: glyph.y * px_per_layout,
1426 });
1427 }
1428 }
1429
1430 symbol.glyph_quads = quads;
1431 }
1432}
1433
1434fn candidate_collision_boxes(
1435 candidate: &SymbolCandidate,
1436 projection: CameraProjection,
1437 anchor_mode: SymbolAnchor,
1438 meters_per_pixel: f64,
1439) -> Vec<SymbolCollisionBox> {
1440 let world = projection.project(&candidate.anchor);
1441 let effective_offset = resolve_symbol_offset(
1442 anchor_mode,
1443 candidate.offset_px,
1444 candidate.radial_offset,
1445 candidate.variable_anchor_offsets.as_deref(),
1446 candidate.size_px,
1447 );
1448 let [half_w_px, half_h_px] = symbol_half_extents(
1449 candidate.size_px,
1450 candidate.text.as_deref(),
1451 candidate.icon_image.as_deref(),
1452 candidate.writing_mode,
1453 candidate.placement,
1454 candidate.text_max_width,
1455 candidate.text_line_height,
1456 candidate.text_letter_spacing,
1457 candidate.icon_text_fit,
1458 candidate.icon_text_fit_padding,
1459 effective_offset,
1460 candidate.padding_px.max(0.0) as f64,
1461 );
1462 let half_w = half_w_px * meters_per_pixel;
1463 let half_h = half_h_px * meters_per_pixel;
1464 let signs = anchor_mode.offset_signs();
1465 let center_x = world.position.x + effective_offset[0] as f64 * meters_per_pixel + signs[0] * half_w * 2.0;
1466 let center_y = world.position.y + effective_offset[1] as f64 * meters_per_pixel + signs[1] * half_h * 2.0;
1467
1468 segmented_collision_boxes(candidate, center_x, center_y, half_w, half_h)
1469}
1470
1471fn segmented_collision_boxes(
1479 candidate: &SymbolCandidate,
1480 center_x: f64,
1481 center_y: f64,
1482 half_w: f64,
1483 half_h: f64,
1484) -> Vec<SymbolCollisionBox> {
1485 if candidate.placement != SymbolPlacement::Line {
1486 return vec![rotated_collision_box(
1487 center_x,
1488 center_y,
1489 half_w,
1490 half_h,
1491 candidate.rotation_rad,
1492 )];
1493 }
1494
1495 let full_width = half_w * 2.0;
1496 let full_height = half_h * 2.0;
1497 let segment_count = ((full_width / full_height.max(1.0)).ceil() as usize).clamp(1, 4);
1498 let segment_half_w = half_w / segment_count as f64;
1499 let sin_theta = candidate.rotation_rad.sin() as f64;
1500 let cos_theta = candidate.rotation_rad.cos() as f64;
1501 let mut boxes = Vec::with_capacity(segment_count);
1502
1503 for index in 0..segment_count {
1504 let local_center_x = -half_w + segment_half_w + (index as f64 * segment_half_w * 2.0);
1505 let rotated_center_x = local_center_x * cos_theta;
1506 let rotated_center_y = local_center_x * sin_theta;
1507 boxes.push(rotated_collision_box(
1508 center_x + rotated_center_x,
1509 center_y + rotated_center_y,
1510 segment_half_w,
1511 half_h,
1512 candidate.rotation_rad,
1513 ));
1514 }
1515
1516 boxes
1517}
1518
1519fn rotated_collision_box(
1526 center_x: f64,
1527 center_y: f64,
1528 half_w: f64,
1529 half_h: f64,
1530 rotation_rad: f32,
1531) -> SymbolCollisionBox {
1532 let sin_theta = rotation_rad.sin() as f64;
1533 let cos_theta = rotation_rad.cos() as f64;
1534 let mut min_x = f64::INFINITY;
1535 let mut min_y = f64::INFINITY;
1536 let mut max_x = f64::NEG_INFINITY;
1537 let mut max_y = f64::NEG_INFINITY;
1538
1539 for [local_x, local_y] in [
1540 [-half_w, -half_h],
1541 [half_w, -half_h],
1542 [half_w, half_h],
1543 [-half_w, half_h],
1544 ] {
1545 let rotated_x = local_x * cos_theta - local_y * sin_theta;
1546 let rotated_y = local_x * sin_theta + local_y * cos_theta;
1547 let x = center_x + rotated_x;
1548 let y = center_y + rotated_y;
1549 min_x = min_x.min(x);
1550 min_y = min_y.min(y);
1551 max_x = max_x.max(x);
1552 max_y = max_y.max(y);
1553 }
1554
1555 SymbolCollisionBox {
1556 min: [min_x, min_y],
1557 max: [max_x, max_y],
1558 }
1559}
1560
1561fn merge_collision_boxes(boxes: &[SymbolCollisionBox]) -> SymbolCollisionBox {
1562 let mut min_x = f64::INFINITY;
1563 let mut min_y = f64::INFINITY;
1564 let mut max_x = f64::NEG_INFINITY;
1565 let mut max_y = f64::NEG_INFINITY;
1566
1567 for bbox in boxes {
1568 min_x = min_x.min(bbox.min[0]);
1569 min_y = min_y.min(bbox.min[1]);
1570 max_x = max_x.max(bbox.max[0]);
1571 max_y = max_y.max(bbox.max[1]);
1572 }
1573
1574 if boxes.is_empty() {
1575 SymbolCollisionBox {
1576 min: [0.0, 0.0],
1577 max: [0.0, 0.0],
1578 }
1579 } else {
1580 SymbolCollisionBox {
1581 min: [min_x, min_y],
1582 max: [max_x, max_y],
1583 }
1584 }
1585}
1586
1587fn ordered_candidate_anchors(candidate: &SymbolCandidate, previous: Option<SymbolAnchor>) -> Vec<SymbolAnchor> {
1588 let mut anchors = candidate.anchors.clone();
1589 if anchors.is_empty() {
1590 anchors.push(SymbolAnchor::Center);
1591 }
1592 if let Some(previous) = previous {
1593 if let Some(index) = anchors.iter().position(|anchor| *anchor == previous) {
1594 anchors.swap(0, index);
1595 }
1596 }
1597 anchors
1598}
1599
1600fn viewport_contains_box(
1601 viewport_bounds: Option<&WorldBounds>,
1602 bbox: &SymbolCollisionBox,
1603 padding_factor: f64,
1604) -> bool {
1605 let Some(bounds) = viewport_bounds else {
1606 return true;
1607 };
1608 let width = (bbox.max[0] - bbox.min[0]).abs() * padding_factor;
1609 let height = (bbox.max[1] - bbox.min[1]).abs() * padding_factor;
1610 !(bbox.max[0] < bounds.min.position.x - width
1611 || bbox.min[0] > bounds.max.position.x + width
1612 || bbox.max[1] < bounds.min.position.y - height
1613 || bbox.min[1] > bounds.max.position.y + height)
1614}
1615
1616fn symbol_half_extents(
1617 size_px: f32,
1618 text: Option<&str>,
1619 icon_image: Option<&str>,
1620 writing_mode: SymbolWritingMode,
1621 placement: SymbolPlacement,
1622 text_max_width: Option<f32>,
1623 text_line_height: Option<f32>,
1624 text_letter_spacing: Option<f32>,
1625 icon_text_fit: SymbolIconTextFit,
1626 icon_text_fit_padding: [f32; 4],
1627 offset_px: [f32; 2],
1628 padding_px: f64,
1629) -> [f64; 2] {
1630 let size_px = size_px.max(1.0) as f64;
1631 let (text_width_px, text_height_px) = estimate_text_box(
1632 text,
1633 size_px,
1634 placement,
1635 text_max_width,
1636 text_line_height,
1637 text_letter_spacing,
1638 );
1639 let icon_size_px = if icon_image.is_some() { size_px * 1.2 } else { 0.0 };
1640 let (width_px, height_px) = match writing_mode {
1641 SymbolWritingMode::Horizontal => fitted_symbol_box(
1642 text_width_px.max(size_px),
1643 text_height_px.max(size_px),
1644 icon_image.is_some(),
1645 icon_size_px,
1646 icon_text_fit,
1647 icon_text_fit_padding,
1648 padding_px,
1649 ),
1650 SymbolWritingMode::Vertical => fitted_symbol_box(
1651 text_height_px.max(size_px),
1652 text_width_px.max(size_px),
1653 icon_image.is_some(),
1654 icon_size_px,
1655 icon_text_fit,
1656 icon_text_fit_padding,
1657 padding_px,
1658 ),
1659 };
1660 [
1661 (width_px + offset_px[0].abs() as f64) * 0.5,
1662 (height_px + offset_px[1].abs() as f64) * 0.5,
1663 ]
1664}
1665
1666fn estimate_text_box(
1677 text: Option<&str>,
1678 size_px: f64,
1679 placement: SymbolPlacement,
1680 text_max_width: Option<f32>,
1681 text_line_height: Option<f32>,
1682 text_letter_spacing: Option<f32>,
1683) -> (f64, f64) {
1684 let Some(text) = text else {
1685 return (0.0, 0.0);
1686 };
1687
1688 let glyph_count = text.chars().count() as f64;
1689 let letter_spacing_px = text_letter_spacing.unwrap_or(0.0).max(0.0) as f64 * size_px;
1690 let total_width_px = glyph_count * size_px * 0.6 + (glyph_count - 1.0).max(0.0) * letter_spacing_px;
1691 let line_height_px = size_px * text_line_height.unwrap_or(1.2).max(0.1) as f64;
1692
1693 if placement != SymbolPlacement::Point {
1694 return (total_width_px, line_height_px);
1695 }
1696
1697 let Some(max_width_em) = text_max_width else {
1698 return (total_width_px, line_height_px);
1699 };
1700 let max_width_px = (max_width_em.max(1.0) as f64) * size_px;
1701 if total_width_px <= max_width_px {
1702 return (total_width_px, line_height_px);
1703 }
1704
1705 let line_count = (total_width_px / max_width_px).ceil().max(1.0);
1706 (max_width_px, line_height_px * line_count)
1707}
1708
1709fn fitted_symbol_box(
1716 text_width_px: f64,
1717 text_height_px: f64,
1718 has_icon: bool,
1719 icon_size_px: f64,
1720 icon_text_fit: SymbolIconTextFit,
1721 icon_text_fit_padding: [f32; 4],
1722 padding_px: f64,
1723) -> (f64, f64) {
1724 if !has_icon {
1725 return (text_width_px + padding_px * 2.0, text_height_px + padding_px * 2.0);
1726 }
1727
1728 let icon_base_width = icon_size_px;
1729 let icon_base_height = icon_size_px;
1730 if icon_text_fit == SymbolIconTextFit::None || text_width_px <= 0.0 || text_height_px <= 0.0 {
1731 return (
1732 text_width_px + icon_base_width + padding_px * 2.0,
1733 text_height_px.max(icon_base_height) + padding_px * 2.0,
1734 );
1735 }
1736
1737 let fit_width = match icon_text_fit {
1738 SymbolIconTextFit::None | SymbolIconTextFit::Height => icon_base_width,
1739 SymbolIconTextFit::Width | SymbolIconTextFit::Both => {
1740 text_width_px + icon_text_fit_padding[1] as f64 + icon_text_fit_padding[3] as f64
1741 }
1742 };
1743 let fit_height = match icon_text_fit {
1744 SymbolIconTextFit::None | SymbolIconTextFit::Width => icon_base_height,
1745 SymbolIconTextFit::Height | SymbolIconTextFit::Both => {
1746 text_height_px + icon_text_fit_padding[0] as f64 + icon_text_fit_padding[2] as f64
1747 }
1748 };
1749
1750 (
1751 fit_width.max(text_width_px) + padding_px * 2.0,
1752 fit_height.max(text_height_px) + padding_px * 2.0,
1753 )
1754}
1755
1756fn resolve_symbol_offset(
1766 anchor_mode: SymbolAnchor,
1767 offset_px: [f32; 2],
1768 radial_offset: Option<f32>,
1769 variable_anchor_offsets: Option<&[(SymbolAnchor, [f32; 2])]>,
1770 size_px: f32,
1771) -> [f32; 2] {
1772 if let Some(anchor_offsets) = variable_anchor_offsets {
1773 if let Some((_, offset)) = anchor_offsets.iter().find(|(anchor, _)| *anchor == anchor_mode) {
1774 return [offset[0] * size_px.max(1.0), offset[1] * size_px.max(1.0)];
1775 }
1776 }
1777 let Some(radial_offset) = radial_offset else {
1778 return resolve_anchor_relative_text_offset(anchor_mode, offset_px);
1779 };
1780
1781 let radial_px = radial_offset.max(0.0) * size_px.max(1.0);
1782 let diagonal_px = radial_px / std::f32::consts::SQRT_2;
1783 match anchor_mode {
1784 SymbolAnchor::Center => [0.0, 0.0],
1785 SymbolAnchor::Top => [0.0, radial_px],
1786 SymbolAnchor::Bottom => [0.0, -radial_px],
1787 SymbolAnchor::Left => [radial_px, 0.0],
1788 SymbolAnchor::Right => [-radial_px, 0.0],
1789 SymbolAnchor::TopLeft => [diagonal_px, diagonal_px],
1790 SymbolAnchor::TopRight => [-diagonal_px, diagonal_px],
1791 SymbolAnchor::BottomLeft => [diagonal_px, -diagonal_px],
1792 SymbolAnchor::BottomRight => [-diagonal_px, -diagonal_px],
1793 }
1794}
1795
1796fn resolve_anchor_relative_text_offset(
1804 anchor_mode: SymbolAnchor,
1805 offset_px: [f32; 2],
1806) -> [f32; 2] {
1807 let offset_x = offset_px[0].abs();
1808 let offset_y = offset_px[1].abs();
1809
1810 match anchor_mode {
1811 SymbolAnchor::Center => offset_px,
1812 SymbolAnchor::Top => [0.0, offset_y],
1813 SymbolAnchor::Bottom => [0.0, -offset_y],
1814 SymbolAnchor::Left => [offset_x, 0.0],
1815 SymbolAnchor::Right => [-offset_x, 0.0],
1816 SymbolAnchor::TopLeft => [offset_x, offset_y],
1817 SymbolAnchor::TopRight => [-offset_x, offset_y],
1818 SymbolAnchor::BottomLeft => [offset_x, -offset_y],
1819 SymbolAnchor::BottomRight => [-offset_x, -offset_y],
1820 }
1821}
1822
1823fn append_symbol_quad(
1824 mesh: &mut crate::layers::VectorMeshData,
1825 symbol: &PlacedSymbol,
1826 projection: CameraProjection,
1827 half_w: f64,
1828 half_h: f64,
1829 mut color: [f32; 4],
1830 opacity: f32,
1831 meters_per_pixel: f64,
1832) {
1833 color[3] *= opacity.clamp(0.0, 1.0);
1834 let scale = projection.scale_factor(&symbol.anchor).max(1e-6);
1835 let offset_scale = 1.0 / scale;
1836 let signs = symbol.anchor_mode.offset_signs();
1837 let center_x = symbol.world_anchor[0]
1838 + symbol.offset_px[0] as f64 * meters_per_pixel * offset_scale
1839 + signs[0] * half_w * 2.0;
1840 let center_y = symbol.world_anchor[1]
1841 + symbol.offset_px[1] as f64 * meters_per_pixel * offset_scale
1842 + signs[1] * half_h * 2.0;
1843 let z = symbol.world_anchor[2];
1844 let sin_theta = symbol.rotation_rad.sin() as f64;
1845 let cos_theta = symbol.rotation_rad.cos() as f64;
1846 let base = mesh.positions.len() as u32;
1847 for [local_x, local_y] in [
1848 [-half_w, -half_h],
1849 [half_w, -half_h],
1850 [half_w, half_h],
1851 [-half_w, half_h],
1852 ] {
1853 let rotated_x = local_x * cos_theta - local_y * sin_theta;
1854 let rotated_y = local_x * sin_theta + local_y * cos_theta;
1855 mesh.positions.push([center_x + rotated_x, center_y + rotated_y, z]);
1856 }
1857 for _ in 0..4 {
1858 mesh.colors.push(color);
1859 }
1860 mesh.indices.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
1861}
1862
1863fn cross_tile_id_for_symbol(
1864 text: Option<&str>,
1865 icon: Option<&str>,
1866 anchor: &GeoCoord,
1867 meters_per_pixel: f64,
1868) -> String {
1869 let world = CameraProjection::WebMercator.project(anchor);
1870 let bucket = (meters_per_pixel * 32.0).max(1.0);
1871 let x = (world.position.x / bucket).round() as i64;
1872 let y = (world.position.y / bucket).round() as i64;
1873 format!("{}|{}|{}|{}", text.unwrap_or(""), icon.unwrap_or(""), x, y)
1874}
1875
1876fn binary_to_sdf(
1886 alpha: &[u8],
1887 width: usize,
1888 height: usize,
1889 buffer: usize,
1890) -> Vec<u8> {
1891 if width == 0 || height == 0 {
1892 return Vec::new();
1893 }
1894 let new_w = width + 2 * buffer;
1895 let new_h = height + 2 * buffer;
1896 let len = new_w * new_h;
1897 let big = (new_w * new_w + new_h * new_h) as f32;
1898
1899 let mut outer = vec![big; len];
1902 let mut inner = vec![0.0f32; len];
1905
1906 for y in 0..height {
1907 for x in 0..width {
1908 let dst = (y + buffer) * new_w + (x + buffer);
1909 if alpha[y * width + x] > 127 {
1910 outer[dst] = 0.0;
1911 inner[dst] = big;
1912 }
1913 }
1914 }
1915
1916 edt_2d(&mut outer, new_w, new_h);
1917 edt_2d(&mut inner, new_w, new_h);
1918
1919 let buf_f = buffer as f32;
1920 let mut sdf = vec![0u8; len];
1921 for i in 0..len {
1922 let dist = outer[i].sqrt() - inner[i].sqrt();
1923 let normalized = 0.5 - dist / (2.0 * buf_f);
1924 sdf[i] = (normalized.clamp(0.0, 1.0) * 255.0) as u8;
1925 }
1926 sdf
1927}
1928
1929fn edt_2d(grid: &mut [f32], width: usize, height: usize) {
1931 for y in 0..height {
1932 let offset = y * width;
1933 edt_1d(&mut grid[offset..offset + width]);
1934 }
1935 let mut col = vec![0.0f32; height];
1936 for x in 0..width {
1937 for y in 0..height {
1938 col[y] = grid[y * width + x];
1939 }
1940 edt_1d(&mut col);
1941 for y in 0..height {
1942 grid[y * width + x] = col[y];
1943 }
1944 }
1945}
1946
1947fn edt_1d(f: &mut [f32]) {
1953 let n = f.len();
1954 if n <= 1 {
1955 return;
1956 }
1957 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;
1962 z[1] = f32::INFINITY;
1963 let mut k: usize = 0;
1964
1965 for q in 1..n {
1966 let q2 = (q * q) as f32;
1967 loop {
1968 let vk = v[k];
1969 let vk2 = (vk * vk) as f32;
1970 let s = ((f[q] + q2) - (f[vk] + vk2)) / (2.0 * (q - vk) as f32);
1971 if s > z[k] {
1972 k += 1;
1973 v[k] = q;
1974 z[k] = s;
1975 z[k + 1] = f32::INFINITY;
1976 break;
1977 }
1978 k -= 1;
1980 }
1981 }
1982
1983 k = 0;
1984 for q in 0..n {
1985 while z[k + 1] < q as f32 {
1986 k += 1;
1987 }
1988 let dq = q as f32 - v[k] as f32;
1989 d[q] = dq * dq + f[v[k]];
1990 }
1991 f.copy_from_slice(&d);
1992}
1993
1994fn blit_alpha(
1995 atlas: &mut [u8],
1996 atlas_width: usize,
1997 origin: [u16; 2],
1998 width: usize,
1999 height: usize,
2000 src: &[u8],
2001) {
2002 for row in 0..height {
2003 let dst_start = (origin[1] as usize + row) * atlas_width + origin[0] as usize;
2004 let src_start = row * width;
2005 atlas[dst_start..dst_start + width].copy_from_slice(&src[src_start..src_start + width]);
2006 }
2007}
2008
2009#[cfg(test)]
2010mod tests {
2011 use super::*;
2012 use crate::camera_projection::CameraProjection;
2013 use rustial_math::GeoCoord;
2014
2015 fn candidate(id: &str, lon: f64, text: &str) -> SymbolCandidate {
2016 SymbolCandidate {
2017 id: id.into(),
2018 layer_id: Some("symbols".into()),
2019 source_id: Some("source".into()),
2020 source_layer: Some("poi".into()),
2021 source_tile: None,
2022 feature_id: id.into(),
2023 feature_index: 0,
2024 placement_group_id: id.into(),
2025 placement: SymbolPlacement::Point,
2026 anchor: GeoCoord::from_lat_lon(0.0, lon),
2027 text: Some(text.into()),
2028 icon_image: None,
2029 font_stack: "Test Sans".into(),
2030 cross_tile_id: id.into(),
2031 rotation_rad: 0.0,
2032 size_px: 16.0,
2033 padding_px: 2.0,
2034 allow_overlap: false,
2035 ignore_placement: false,
2036 sort_key: None,
2037 radial_offset: None,
2038 variable_anchor_offsets: None,
2039 text_max_width: None,
2040 text_line_height: None,
2041 text_letter_spacing: None,
2042 icon_text_fit: SymbolIconTextFit::None,
2043 icon_text_fit_padding: [0.0, 0.0, 0.0, 0.0],
2044 anchors: vec![SymbolAnchor::Center],
2045 writing_mode: SymbolWritingMode::Horizontal,
2046 offset_px: [0.0, 0.0],
2047 fill_color: [1.0, 1.0, 1.0, 1.0],
2048 halo_color: [0.0, 0.0, 0.0, 1.0],
2049 }
2050 }
2051
2052 #[test]
2053 fn glyph_atlas_tracks_unique_glyphs() {
2054 let mut atlas = GlyphAtlas::new();
2055 atlas.request_text("Test Sans", "aba");
2056 assert_eq!(atlas.len(), 2);
2057 atlas.load_requested(&ProceduralGlyphProvider::new());
2058 assert_eq!(atlas.entries().count(), 2);
2059 assert!(atlas.dimensions()[0] > 0);
2060 }
2061
2062 #[test]
2063 fn image_manager_tracks_referenced_ids() {
2064 let mut images = ImageManager::new();
2065 images.register_image(SpriteImage::new("marker", [32, 32]));
2066 images.request("marker");
2067 assert!(images.contains("marker"));
2068 assert_eq!(images.referenced().collect::<Vec<_>>(), vec!["marker"]);
2069 }
2070
2071 #[test]
2072 fn placement_filters_colliding_symbols() {
2073 let mut engine = SymbolPlacementEngine::new();
2074 let placed = engine.place_candidates(&[candidate("a", 0.0, "Alpha"), candidate("b", 0.00001, "Beta")], CameraProjection::WebMercator, 2.0, 1.0 / 60.0, None);
2075 assert_eq!(placed.iter().filter(|symbol| symbol.visible).count(), 1);
2076 }
2077
2078 #[test]
2079 fn placement_dedupes_nearby_repeated_labels() {
2080 let mut engine = SymbolPlacementEngine::new();
2081 let placed = engine.place_candidates(&[candidate("a", 0.0, "Same"), candidate("b", 0.0, "Same")], CameraProjection::WebMercator, 2.0, 1.0 / 60.0, None);
2082 assert_eq!(placed.len(), 1);
2083 }
2084
2085 #[test]
2086 fn placement_tries_alternate_anchors() {
2087 let mut a = candidate("a", 0.0, "Alpha");
2088 let mut b = candidate("b", 0.0, "Beta");
2089 a.anchors = vec![SymbolAnchor::Center];
2090 b.anchors = vec![SymbolAnchor::Center, SymbolAnchor::Top];
2091
2092 let mut engine = SymbolPlacementEngine::new();
2093 let placed = engine.place_candidates(&[a, b], CameraProjection::WebMercator, 2.0, 1.0, None);
2094 assert_eq!(placed.iter().filter(|symbol| symbol.visible).count(), 2);
2095 assert_eq!(placed[1].anchor_mode, SymbolAnchor::Top);
2096 }
2097
2098 #[test]
2099 fn placement_preserves_previous_anchor_by_cross_tile_id() {
2100 let mut first = candidate("a", 0.0, "Alpha");
2101 first.cross_tile_id = "shared".into();
2102 first.anchors = vec![SymbolAnchor::Center, SymbolAnchor::Top];
2103
2104 let mut blocker = candidate("blocker", 0.0, "Block");
2105 blocker.anchors = vec![SymbolAnchor::Center];
2106
2107 let mut engine = SymbolPlacementEngine::new();
2108 let placed = engine.place_candidates(&[blocker.clone(), first.clone()], CameraProjection::WebMercator, 2.0, 1.0, None);
2109 assert_eq!(placed[1].anchor_mode, SymbolAnchor::Top);
2110
2111 let mut second = candidate("b", 0.0, "Alpha");
2112 second.cross_tile_id = "shared".into();
2113 second.anchors = vec![SymbolAnchor::Center, SymbolAnchor::Top];
2114 let placed = engine.place_candidates(&[blocker, second], CameraProjection::WebMercator, 2.0, 1.0 / 60.0, None);
2115 assert_eq!(placed[1].anchor_mode, SymbolAnchor::Top);
2116 }
2117
2118 #[test]
2119 fn placement_honors_symbol_sort_key_order() {
2120 let mut low = candidate("low", 0.0, "Low");
2121 low.sort_key = Some(0.0);
2122
2123 let mut high = candidate("high", 0.0, "High");
2124 high.sort_key = Some(10.0);
2125
2126 let mut engine = SymbolPlacementEngine::new();
2127 let placed = engine.place_candidates(
2128 &[high, low],
2129 CameraProjection::WebMercator,
2130 2.0,
2131 1.0,
2132 None,
2133 );
2134
2135 assert_eq!(placed.iter().filter(|symbol| symbol.visible).count(), 1);
2136 assert_eq!(placed.iter().find(|symbol| symbol.visible).map(|symbol| symbol.id.as_str()), Some("low"));
2137 }
2138
2139 #[test]
2140 fn fixed_offset_is_resolved_relative_to_anchor_direction() {
2141 assert_eq!(
2142 resolve_symbol_offset(SymbolAnchor::TopRight, [3.0, 4.0], None, None, 10.0),
2143 [-3.0, 4.0]
2144 );
2145 assert_eq!(
2146 resolve_symbol_offset(SymbolAnchor::BottomLeft, [3.0, 4.0], None, None, 10.0),
2147 [3.0, -4.0]
2148 );
2149 }
2150
2151 #[test]
2152 fn centered_fixed_offset_preserves_raw_vector() {
2153 assert_eq!(
2154 resolve_symbol_offset(SymbolAnchor::Center, [3.0, -4.0], None, None, 10.0),
2155 [3.0, -4.0]
2156 );
2157 }
2158
2159 #[test]
2160 fn radial_offset_overrides_fixed_offset_for_anchor_direction() {
2161 let resolved = resolve_symbol_offset(
2162 SymbolAnchor::TopRight,
2163 [99.0, 99.0],
2164 Some(2.0),
2165 None,
2166 10.0,
2167 );
2168
2169 assert!(resolved[0] < 0.0);
2170 assert!(resolved[1] > 0.0);
2171 assert!(resolved[0].abs() < 99.0);
2172 assert!(resolved[1].abs() < 99.0);
2173 }
2174
2175 #[test]
2176 fn variable_anchor_offset_overrides_radial_and_fixed_offsets() {
2177 let resolved = resolve_symbol_offset(
2178 SymbolAnchor::Top,
2179 [99.0, 99.0],
2180 Some(5.0),
2181 Some(&[(SymbolAnchor::Top, [1.0, 2.0])]),
2182 10.0,
2183 );
2184
2185 assert_eq!(resolved, [10.0, 20.0]);
2186 }
2187
2188 #[test]
2189 fn point_text_max_width_wraps_placeholder_text_box() {
2190 let single_line = estimate_text_box(
2191 Some("abcdefghij"),
2192 10.0,
2193 SymbolPlacement::Point,
2194 None,
2195 None,
2196 None,
2197 );
2198 let wrapped = estimate_text_box(
2199 Some("abcdefghij"),
2200 10.0,
2201 SymbolPlacement::Point,
2202 Some(3.0),
2203 None,
2204 None,
2205 );
2206
2207 assert!(wrapped.0 < single_line.0);
2208 assert!(wrapped.1 > single_line.1);
2209 }
2210
2211 #[test]
2212 fn line_text_max_width_does_not_wrap_placeholder_text_box() {
2213 let unbounded = estimate_text_box(
2214 Some("abcdefghij"),
2215 10.0,
2216 SymbolPlacement::Line,
2217 None,
2218 None,
2219 None,
2220 );
2221 let bounded = estimate_text_box(
2222 Some("abcdefghij"),
2223 10.0,
2224 SymbolPlacement::Line,
2225 Some(3.0),
2226 None,
2227 None,
2228 );
2229
2230 assert_eq!(bounded, unbounded);
2231 }
2232
2233 #[test]
2234 fn wrapped_text_box_uses_text_line_height() {
2235 let compact = estimate_text_box(
2236 Some("abcdefghij"),
2237 10.0,
2238 SymbolPlacement::Point,
2239 Some(3.0),
2240 Some(1.0),
2241 None,
2242 );
2243 let spacious = estimate_text_box(
2244 Some("abcdefghij"),
2245 10.0,
2246 SymbolPlacement::Point,
2247 Some(3.0),
2248 Some(2.0),
2249 None,
2250 );
2251
2252 assert_eq!(compact.0, spacious.0);
2253 assert!(spacious.1 > compact.1);
2254 }
2255
2256 #[test]
2257 fn text_letter_spacing_expands_placeholder_text_box_width() {
2258 let compact = estimate_text_box(
2259 Some("abcde"),
2260 10.0,
2261 SymbolPlacement::Point,
2262 None,
2263 None,
2264 None,
2265 );
2266 let spaced = estimate_text_box(
2267 Some("abcde"),
2268 10.0,
2269 SymbolPlacement::Point,
2270 None,
2271 None,
2272 Some(0.25),
2273 );
2274
2275 assert!(spaced.0 > compact.0);
2276 assert_eq!(spaced.1, compact.1);
2277 }
2278
2279 #[test]
2280 fn icon_text_fit_both_wraps_icon_around_text_box() {
2281 let fitted = symbol_half_extents(
2282 10.0,
2283 Some("abcdefghij"),
2284 Some("marker"),
2285 SymbolWritingMode::Horizontal,
2286 SymbolPlacement::Point,
2287 Some(3.0),
2288 Some(1.5),
2289 Some(0.25),
2290 SymbolIconTextFit::Both,
2291 [1.0, 2.0, 3.0, 4.0],
2292 [0.0, 0.0],
2293 0.0,
2294 );
2295 let unfitted = symbol_half_extents(
2296 10.0,
2297 Some("abcdefghij"),
2298 Some("marker"),
2299 SymbolWritingMode::Horizontal,
2300 SymbolPlacement::Point,
2301 Some(3.0),
2302 Some(1.5),
2303 Some(0.25),
2304 SymbolIconTextFit::None,
2305 [0.0, 0.0, 0.0, 0.0],
2306 [0.0, 0.0],
2307 0.0,
2308 );
2309
2310 assert!(fitted[0] < unfitted[0]);
2311 assert!(fitted[1] > unfitted[1]);
2312 }
2313
2314 #[test]
2315 fn icon_text_fit_width_only_keeps_base_height() {
2316 let fitted = fitted_symbol_box(
2317 40.0,
2318 18.0,
2319 true,
2320 12.0,
2321 SymbolIconTextFit::Width,
2322 [2.0, 3.0, 4.0, 5.0],
2323 0.0,
2324 );
2325
2326 assert_eq!(fitted.1, 18.0);
2327 assert_eq!(fitted.0, 48.0);
2328 }
2329
2330 #[test]
2331 fn placement_ignored_symbol_does_not_block_later_symbol() {
2332 let mut first = candidate("first", 0.0, "Alpha");
2333 first.ignore_placement = true;
2334
2335 let second = candidate("second", 0.0, "Beta");
2336
2337 let mut engine = SymbolPlacementEngine::new();
2338 let placed = engine.place_candidates(
2339 &[first, second],
2340 CameraProjection::WebMercator,
2341 2.0,
2342 1.0,
2343 None,
2344 );
2345
2346 assert_eq!(placed.iter().filter(|symbol| symbol.visible).count(), 2);
2347 }
2348
2349 #[test]
2350 fn asset_registry_rebuilds_from_visible_symbols() {
2351 let mut registry = SymbolAssetRegistry::new();
2352 let mut engine = SymbolPlacementEngine::new();
2353 let placed = engine.place_candidates(
2354 &[SymbolCandidate {
2355 id: "icon".into(),
2356 layer_id: Some("symbols".into()),
2357 source_id: Some("source".into()),
2358 source_layer: Some("poi".into()),
2359 source_tile: None,
2360 feature_id: "icon".into(),
2361 feature_index: 0,
2362 placement_group_id: "icon".into(),
2363 placement: SymbolPlacement::Point,
2364 anchor: GeoCoord::from_lat_lon(0.0, 0.0),
2365 text: Some("Hi".into()),
2366 icon_image: Some("marker".into()),
2367 font_stack: "Test Sans".into(),
2368 cross_tile_id: "icon".into(),
2369 rotation_rad: 0.0,
2370 size_px: 14.0,
2371 padding_px: 0.0,
2372 allow_overlap: true,
2373 ignore_placement: false,
2374 sort_key: None,
2375 radial_offset: None,
2376 variable_anchor_offsets: None,
2377 text_max_width: None,
2378 text_line_height: None,
2379 text_letter_spacing: None,
2380 icon_text_fit: SymbolIconTextFit::None,
2381 icon_text_fit_padding: [0.0, 0.0, 0.0, 0.0],
2382 anchors: vec![SymbolAnchor::Center],
2383 writing_mode: SymbolWritingMode::Horizontal,
2384 offset_px: [0.0, 0.0],
2385 fill_color: [1.0, 1.0, 1.0, 1.0],
2386 halo_color: [0.0, 0.0, 0.0, 1.0],
2387 }],
2388 CameraProjection::WebMercator,
2389 1.0,
2390 0.5,
2391 None,
2392 );
2393 registry.rebuild_from_symbols(&placed);
2394 assert!(registry.glyphs().len() >= 2);
2395 assert_eq!(registry.images().referenced().collect::<Vec<_>>(), vec!["marker"]);
2396 }
2397
2398 #[test]
2399 fn placed_symbols_generate_render_mesh() {
2400 let mut engine = SymbolPlacementEngine::new();
2401 let placed = engine.place_candidates(&[candidate("a", 0.0, "Alpha")], CameraProjection::WebMercator, 1.0, 1.0, None);
2402 let mesh = symbol_mesh_from_placed_symbols(&placed, CameraProjection::WebMercator, 1.0);
2403 assert_eq!(mesh.vertex_count(), 8);
2404 assert_eq!(mesh.index_count(), 12);
2405 }
2406
2407 #[test]
2408 fn rotated_collision_box_expands_for_diagonal_symbols() {
2409 let bbox = rotated_collision_box(0.0, 0.0, 10.0, 2.0, std::f32::consts::FRAC_PI_4);
2410 let width = bbox.max[0] - bbox.min[0];
2414 let height = bbox.max[1] - bbox.min[1];
2415 assert!(width > 4.0, "rotated AABB width {width} should exceed the unrotated height");
2416 assert!(height > 4.0, "rotated AABB height {height} should exceed the unrotated height");
2417 assert!((width - height).abs() < 1.0, "45 deg rotation should produce a near-square AABB");
2419 }
2420
2421 #[test]
2422 fn line_candidates_use_multiple_collision_boxes() {
2423 let mut line = candidate("line", 0.0, "Long river label");
2424 line.placement = SymbolPlacement::Line;
2425 line.rotation_rad = std::f32::consts::FRAC_PI_4;
2426
2427 let boxes = candidate_collision_boxes(&line, CameraProjection::WebMercator, SymbolAnchor::Center, 2.0);
2428 assert!(boxes.len() > 1);
2429 }
2430
2431 #[test]
2432 fn rotated_line_candidates_can_coexist_when_segment_boxes_no_longer_overlap() {
2433 let mut a = candidate("a", 0.0, "Alpha");
2434 a.placement = SymbolPlacement::Line;
2435 a.rotation_rad = std::f32::consts::FRAC_PI_2;
2436
2437 let mut b = candidate("b", 0.0005, "Beta");
2438 b.placement = SymbolPlacement::Line;
2439 b.rotation_rad = std::f32::consts::FRAC_PI_2;
2440
2441 let mut engine = SymbolPlacementEngine::new();
2442 let placed = engine.place_candidates(
2443 &[a, b],
2444 CameraProjection::WebMercator,
2445 2.0,
2446 1.0,
2447 None,
2448 );
2449 assert_eq!(placed.iter().filter(|symbol| symbol.visible).count(), 2);
2450 }
2451
2452 #[test]
2453 fn equirectangular_symbol_anchor_differs_from_mercator() {
2454 let mut engine = SymbolPlacementEngine::new();
2455 let mut merc_candidate = candidate("a", 10.0, "Alpha");
2456 merc_candidate.anchor = GeoCoord::from_lat_lon(45.0, 10.0);
2457 let mut eq_candidate = candidate("b", 10.0, "Alpha");
2458 eq_candidate.anchor = GeoCoord::from_lat_lon(45.0, 10.0);
2459 let merc = engine.place_candidates(&[merc_candidate], CameraProjection::WebMercator, 1.0, 1.0, None);
2460 let eq = engine.place_candidates(&[eq_candidate], CameraProjection::Equirectangular, 1.0, 1.0, None);
2461 assert_eq!(merc.len(), 1);
2462 assert_eq!(eq.len(), 1);
2463 assert!((merc[0].world_anchor[1] - eq[0].world_anchor[1]).abs() > 1.0);
2464 }
2465
2466 #[test]
2469 fn sdf_single_pixel_inside_produces_centered_gradient() {
2470 let alpha = vec![255u8];
2473 let sdf = binary_to_sdf(&alpha, 1, 1, 3);
2474 let w = 1 + 2 * 3; assert_eq!(sdf.len(), w * w);
2476 let center = sdf[3 * w + 3];
2478 assert!(center > 140, "center SDF value {center} should be inside (>140)");
2479 let corner = sdf[0];
2481 assert!(corner < 100, "corner SDF value {corner} should be outside (<100)");
2482 }
2483
2484 #[test]
2485 fn sdf_empty_bitmap_is_all_outside() {
2486 let alpha = vec![0u8; 4];
2488 let sdf = binary_to_sdf(&alpha, 2, 2, 2);
2489 for &v in &sdf {
2490 assert!(v <= 128, "SDF value {v} should be ≤128 for all-outside");
2491 }
2492 }
2493
2494 #[test]
2495 fn sdf_full_bitmap_is_all_inside() {
2496 let alpha = vec![255u8; 4];
2498 let sdf = binary_to_sdf(&alpha, 2, 2, 2);
2499 let w = 2 + 2 * 2;
2500 let interior = sdf[(2) * w + (2)]; assert!(interior > 128, "SDF interior {interior} should be >128");
2503 }
2504
2505 #[test]
2506 fn sdf_adds_padding_to_dimensions() {
2507 let alpha = vec![255u8; 6]; let sdf = binary_to_sdf(&alpha, 3, 2, 3);
2509 assert_eq!(sdf.len(), (3 + 6) * (2 + 6)); }
2511
2512 #[test]
2513 fn sdf_zero_size_returns_empty() {
2514 assert!(binary_to_sdf(&[], 0, 0, 3).is_empty());
2515 }
2516
2517 #[test]
2518 fn glyph_atlas_sdf_entries_include_padding() {
2519 let mut atlas = GlyphAtlas::new();
2522 atlas.request_text("Test Sans", "A");
2523 atlas.load_requested(&ProceduralGlyphProvider::new());
2524 let entry = atlas.get("Test Sans", 'A').expect("glyph A");
2525 assert_eq!(entry.size[0], 8 + 2 * SDF_BUFFER);
2527 assert_eq!(entry.size[1], 12 + 2 * SDF_BUFFER);
2528 }
2529
2530 #[test]
2531 fn glyph_atlas_sdf_alpha_contains_gradient_values() {
2532 let mut atlas = GlyphAtlas::new();
2534 atlas.request_text("Test Sans", "X");
2535 atlas.load_requested(&ProceduralGlyphProvider::new());
2536 let has_intermediate = atlas.alpha().iter().any(|&v| v > 10 && v < 245);
2537 assert!(has_intermediate, "SDF atlas should contain gradient values between 0 and 255");
2538 }
2539
2540 #[test]
2541 fn glyph_atlas_render_em_px_from_procedural() {
2542 let mut atlas = GlyphAtlas::new();
2543 atlas.request_text("Test Sans", "A");
2544 atlas.load_requested(&ProceduralGlyphProvider::new());
2545 assert_eq!(atlas.render_em_px(), 12.0);
2546 }
2547
2548 #[test]
2549 fn edt_1d_seeds_remain_zero() {
2550 let mut f = vec![0.0, 1e10, 1e10, 0.0, 1e10];
2551 edt_1d(&mut f);
2552 assert_eq!(f[0], 0.0);
2553 assert_eq!(f[3], 0.0);
2554 assert!(f[1] > 0.0);
2555 assert!(f[2] > 0.0);
2556 }
2557
2558 #[test]
2559 fn edt_1d_distances_increase_from_seed() {
2560 let mut f = vec![0.0, 1e10, 1e10, 1e10, 1e10];
2561 edt_1d(&mut f);
2562 assert!((f[0] - 0.0).abs() < 0.01);
2564 assert!((f[1] - 1.0).abs() < 0.01);
2565 assert!((f[2] - 4.0).abs() < 0.01);
2566 assert!((f[3] - 9.0).abs() < 0.01);
2567 assert!((f[4] - 16.0).abs() < 0.01);
2568 }
2569}