//! Zoom-constant vector POI symbols. //! //! The openstreetmap-carto symbol SVGs (CC0) are tessellated ONCE at their //! final on-screen size and appended into the tile's cached vector buffers as //! anchor + screen-px-offset vertices; the map vertex shader adds the offset //! AFTER the map transform, so symbols keep a constant pixel size at every //! zoom — pure vector, no raster atlas. (The same encoding can later back a //! single-quad fragment-side curve evaluator without changing the tile data.) use makepad_widgets::makepad_draw::vector::{ document::SvgNode, parse::parse_svg, LineJoin, PathCmd, Tessellator, VVertex, VectorPath, }; use std::collections::HashMap; use std::sync::OnceLock; /// On-screen symbol size; carto icons are authored at 14x14. pub const ICON_SIZE_PX: f32 = 14.0; /// Symbols appear from this view-zoom bucket (carto shows the full POI /// symbol set from z17). pub const ICON_MIN_ZOOM: u32 = 17; // Label color classes for POI icons pub const LABEL_CLASS_DEFAULT: u8 = 0; pub const LABEL_CLASS_AMENITY: u8 = 1; pub const LABEL_CLASS_SHOP: u8 = 2; pub const LABEL_CLASS_TRANSPORT: u8 = 3; pub const LABEL_CLASS_CULTURE: u8 = 4; pub const LABEL_CLASS_GREEN: u8 = 5; pub const LABEL_CLASS_MUTED: u8 = 6; pub const LABEL_CLASS_TREE: u8 = 7; pub const LABEL_CLASS_HEALTH: u8 = 8; pub struct IconMesh { pub verts: Vec, pub indices: Vec, } const ICON_SVGS: &[(&str, &str)] = &[ ("alcohol", include_str!("icons/alcohol.svg")), ("atm", include_str!("icons/atm.svg")), ("bakery", include_str!("icons/bakery.svg")), ("bank", include_str!("icons/bank.svg")), ("bar", include_str!("icons/bar.svg")), ("beauty", include_str!("icons/beauty.svg")), ("bicycle", include_str!("icons/bicycle.svg")), ("butcher", include_str!("icons/butcher.svg")), ("cafe", include_str!("icons/cafe.svg")), ("cinema", include_str!("icons/cinema.svg")), ("clothes", include_str!("icons/clothes.svg")), ("coffee", include_str!("icons/coffee.svg")), ("convenience", include_str!("icons/convenience.svg")), ("fast_food", include_str!("icons/fast_food.svg")), ("florist", include_str!("icons/florist.svg")), ("gift", include_str!("icons/gift.svg")), ("greengrocer", include_str!("icons/greengrocer.svg")), ("hairdresser", include_str!("icons/hairdresser.svg")), ("hotel", include_str!("icons/hotel.svg")), ("ice_cream", include_str!("icons/ice_cream.svg")), ("laundry", include_str!("icons/laundry.svg")), ("library", include_str!("icons/library.svg")), ("museum", include_str!("icons/museum.svg")), ("nightclub", include_str!("icons/nightclub.svg")), ("pharmacy", include_str!("icons/pharmacy.svg")), ("place_of_worship", include_str!("icons/place_of_worship.svg")), ("pub", include_str!("icons/pub.svg")), ("restaurant", include_str!("icons/restaurant.svg")), ("supermarket", include_str!("icons/supermarket.svg")), ("theatre", include_str!("icons/theatre.svg")), // Micro-POIs from the all-tag detail archive ("bench", include_str!("icons/bench.svg")), ("waste_basket", include_str!("icons/waste_basket.svg")), ("recycling", include_str!("icons/recycling.svg")), ("playground", include_str!("icons/playground.svg")), ("statue", include_str!("icons/statue.svg")), ("entrance", include_str!("icons/entrance.svg")), ("information", include_str!("icons/information.svg")), ("traffic_signals", include_str!("icons/traffic_signals.svg")), ("charger_pin", include_str!("icons/charger_pin.svg")), ("parking", include_str!("icons/parking.svg")), ("charger", include_str!("icons/charger.svg")), ]; fn icons() -> &'static HashMap<&'static str, IconMesh> { static CACHE: OnceLock> = OnceLock::new(); CACHE.get_or_init(|| { let mut out = HashMap::new(); for (name, svg) in ICON_SVGS { if let Some(mesh) = build_icon_mesh(svg) { out.insert(*name, mesh); } } // Trees render as plain canopy discs (carto draws them as circles, // not glyphs). if let Some(mesh) = build_disc_mesh(5.4) { out.insert("tree", mesh); } // Generic small dot for named POIs with no dedicated symbol. if let Some(mesh) = build_disc_mesh(2.4) { out.insert("dot", mesh); } // Dark center dot layered over the light tree canopy. if let Some(mesh) = build_disc_mesh(1.7) { out.insert("tree_core", mesh); } // Tesla-style charger pins: wide badge (bolt + kW text) for fast // sites, small badge for street AC; white bolt overlays. The mesh // is shifted so the TAIL TIP sits exactly on the anchor (the site): // a centered mesh made the tip sweep across the ground when the // camera rotated in a tilted view. // wide: viewBox 34x30, tip (17,24), scale 30/34 -> tip at +7.94 if let Some(mesh) = build_icon_mesh_sized_offset(include_str!("icons/charger_pin_wide.svg"), 30.0, 0.0, -7.94) { out.insert("charger_pin_fast", mesh); } // small: viewBox 22x22, tip (11,20.5), scale 16/22 -> tip at +6.91 if let Some(mesh) = build_icon_mesh_sized_offset(include_str!("icons/charger_pin.svg"), 16.0, 0.0, -6.91) { out.insert("charger_pin_ac", mesh); } // Bolt overlays carry their in-pin offset IN THE MESH (screen px): // offsetting the anchor instead scales with the map and the pin // composite smears apart at fractional zooms. if let Some(mesh) = build_icon_mesh_sized_offset(include_str!("icons/charger.svg"), 9.0, -8.5, -12.35) { out.insert("charger_bolt_fast", mesh); } if let Some(mesh) = build_icon_mesh_sized_offset(include_str!("icons/charger.svg"), 9.0, 0.0, -8.36) { out.insert("charger_bolt_ac", mesh); } out }) } pub fn icon_mesh(name: &str) -> Option<&'static IconMesh> { icons().get(name) } fn transform_coord(value: f32, center: f32, scale: f32) -> f32 { (value - center) * scale } fn collect_paths(nodes: &[SvgNode], out: &mut VectorPath) { for node in nodes { match node { SvgNode::Path(p) => { for cmd in &p.path.cmds { let t = &p.transform; out.cmds.push(match *cmd { PathCmd::MoveTo(x, y) => { let (x, y) = t.apply(x, y); PathCmd::MoveTo(x, y) } PathCmd::LineTo(x, y) => { let (x, y) = t.apply(x, y); PathCmd::LineTo(x, y) } PathCmd::BezierTo(x1, y1, x2, y2, x, y) => { let (x1, y1) = t.apply(x1, y1); let (x2, y2) = t.apply(x2, y2); let (x, y) = t.apply(x, y); PathCmd::BezierTo(x1, y1, x2, y2, x, y) } PathCmd::Close => PathCmd::Close, PathCmd::Winding(w) => PathCmd::Winding(w), }); } } SvgNode::Group(g) => collect_paths(&g.children, out), _ => {} } } } /// Parse + tessellate one symbol at its final screen size, centered on the /// origin so vertices double as screen-px offsets from the anchor point. fn build_icon_mesh(svg: &str) -> Option { build_icon_mesh_sized(svg, ICON_SIZE_PX) } fn build_icon_mesh_sized_offset( svg: &str, size_px: f32, dx: f32, dy: f32, ) -> Option { let mut mesh = build_icon_mesh_sized(svg, size_px)?; for vertex in &mut mesh.verts { vertex.x += dx; vertex.y += dy; } Some(mesh) } fn build_icon_mesh_sized(svg: &str, size_px: f32) -> Option { let doc = parse_svg(svg); let (width, height) = doc.logical_size(); if width <= 0.0 || height <= 0.0 { return None; } let mut path = VectorPath::new(); collect_paths(&doc.root, &mut path); if path.cmds.is_empty() { return None; } let scale = size_px / width.max(height); let (center_x, center_y) = (width * 0.5, height * 0.5); for cmd in &mut path.cmds { match cmd { PathCmd::MoveTo(x, y) | PathCmd::LineTo(x, y) => { *x = transform_coord(*x, center_x, scale); *y = transform_coord(*y, center_y, scale); } PathCmd::BezierTo(x1, y1, x2, y2, x, y) => { *x1 = transform_coord(*x1, center_x, scale); *y1 = transform_coord(*y1, center_y, scale); *x2 = transform_coord(*x2, center_x, scale); *y2 = transform_coord(*y2, center_y, scale); *x = transform_coord(*x, center_x, scale); *y = transform_coord(*y, center_y, scale); } PathCmd::Close | PathCmd::Winding(_) => {} } } let mut tess = Tessellator::default(); let mut verts = Vec::new(); let mut indices = Vec::new(); tess.flatten(&path, 0.05); tess.fill(1.0, LineJoin::Miter, 4.0, false, &mut verts, &mut indices); if verts.is_empty() || indices.is_empty() { return None; } Some(IconMesh { verts, indices }) } /// Circle mesh for canopy/dot symbols, centered on the origin like the SVG /// icons (vertices double as screen-px offsets from the anchor). fn build_disc_mesh(radius: f32) -> Option { const K: f32 = 0.552_284_75; let r = radius; let k = r * K; let mut path = VectorPath::new(); path.cmds.push(PathCmd::MoveTo(r, 0.0)); path.cmds.push(PathCmd::BezierTo(r, k, k, r, 0.0, r)); path.cmds.push(PathCmd::BezierTo(-k, r, -r, k, -r, 0.0)); path.cmds.push(PathCmd::BezierTo(-r, -k, -k, -r, 0.0, -r)); path.cmds.push(PathCmd::BezierTo(k, -r, r, -k, r, 0.0)); path.cmds.push(PathCmd::Close); let mut tess = Tessellator::default(); let mut verts = Vec::new(); let mut indices = Vec::new(); tess.flatten(&path, 0.05); tess.fill(1.0, LineJoin::Miter, 4.0, false, &mut verts, &mut indices); if verts.is_empty() || indices.is_empty() { return None; } Some(IconMesh { verts, indices }) } /// Micro-POI symbols sourced from the all-tag detail archive (not present in /// shortbread pois): trees, benches, bins, recycling, playgrounds, artwork. pub fn micro_icon_for_tags(tags: &HashMap) -> Option<(&'static str, u8)> { if tags.get("natural").map(|v| v.as_str()) == Some("tree") { return Some(("tree", LABEL_CLASS_TREE)); } if let Some(amenity) = tags.get("amenity") { return match amenity.as_str() { "bench" => Some(("bench", LABEL_CLASS_MUTED)), "waste_basket" | "waste_disposal" => Some(("waste_basket", LABEL_CLASS_MUTED)), "recycling" => Some(("recycling", LABEL_CLASS_MUTED)), "bicycle_parking" => Some(("bicycle", LABEL_CLASS_TRANSPORT)), "parking" => Some(("parking", LABEL_CLASS_TRANSPORT)), "parking_entrance" => Some(("parking", LABEL_CLASS_TRANSPORT)), "charging_station" => Some(("charger", LABEL_CLASS_TRANSPORT)), _ => None, }; } if tags.get("highway").map(|v| v.as_str()) == Some("traffic_signals") { return Some(("traffic_signals", LABEL_CLASS_MUTED)); } // Offices (TomTom etc.) only exist in the detail archive; carto shows // them as a small dot + name from street-level zoom. if tags.contains_key("office") && tags.contains_key("name") { return Some(("dot", LABEL_CLASS_MUTED)); } if let Some(leisure) = tags.get("leisure") { return match leisure.as_str() { "playground" => Some(("playground", LABEL_CLASS_GREEN)), "picnic_table" => Some(("bench", LABEL_CLASS_GREEN)), _ => None, }; } if let Some(tourism) = tags.get("tourism") { return match tourism.as_str() { "artwork" => Some(("statue", LABEL_CLASS_CULTURE)), "information" => Some(("information", LABEL_CLASS_CULTURE)), _ => None, }; } // Building/station entrances (door icon, high zoom only — the caller // gates the zoom). if tags.get("railway").map(|v| v.as_str()) == Some("subway_entrance") { return Some(("entrance", LABEL_CLASS_TRANSPORT)); } if let Some(entrance) = tags.get("entrance") { return match entrance.as_str() { "no" => None, _ => Some(("entrance", LABEL_CLASS_MUTED)), }; } if let Some(historic) = tags.get("historic") { return match historic.as_str() { "memorial" | "monument" | "statue" => Some(("statue", LABEL_CLASS_CULTURE)), _ => None, }; } None } /// Map shortbread poi attributes to a symbol + label color class. pub fn icon_for_tags(tags: &HashMap) -> Option<(&'static str, u8)> { match tags.get("layer").map(|v| v.as_str()) { Some("micro_pois") => return micro_icon_for_tags(tags), // Geodata overlays (layers.md). Charger pins color by BRAND — // red is exclusively Tesla Superchargers; other brands split // fast DC amber / street AC blue (kW still shows in the bubble). Some("chargers") => { let kw = tags .get("max_kw") .and_then(|v| v.parse::().ok()) .unwrap_or(0.0); let is_tesla = tags .get("operator") .or_else(|| tags.get("brand")) .is_some_and(|value| value.to_lowercase().contains("tesla")); let class = if is_tesla { LABEL_CLASS_HEALTH } else if kw >= 50.0 { LABEL_CLASS_AMENITY } else { LABEL_CLASS_TRANSPORT }; return Some(("charger", class)); } Some("stops") => return Some(("dot", LABEL_CLASS_TRANSPORT)), _ => {} } if let Some(shop) = tags.get("shop") { let name = match shop.as_str() { "supermarket" => "supermarket", "bakery" => "bakery", "butcher" => "butcher", "clothes" | "fashion" | "shoes" => "clothes", "hairdresser" => "hairdresser", "coffee" => "coffee", "alcohol" | "wine" => "alcohol", "beauty" | "cosmetics" => "beauty", "bicycle" => "bicycle", "florist" => "florist", "gift" => "gift", "greengrocer" => "greengrocer", "laundry" | "dry_cleaning" => "laundry", _ => "convenience", }; return Some((name, LABEL_CLASS_SHOP)); } if let Some(amenity) = tags.get("amenity") { return match amenity.as_str() { "restaurant" | "food_court" => Some(("restaurant", LABEL_CLASS_AMENITY)), "cafe" => Some(("cafe", LABEL_CLASS_AMENITY)), "fast_food" => Some(("fast_food", LABEL_CLASS_AMENITY)), "bar" => Some(("bar", LABEL_CLASS_AMENITY)), "pub" | "biergarten" => Some(("pub", LABEL_CLASS_AMENITY)), "ice_cream" => Some(("ice_cream", LABEL_CLASS_AMENITY)), "nightclub" => Some(("nightclub", LABEL_CLASS_AMENITY)), "pharmacy" => Some(("pharmacy", LABEL_CLASS_DEFAULT)), "bank" => Some(("bank", LABEL_CLASS_DEFAULT)), "atm" => Some(("atm", LABEL_CLASS_DEFAULT)), "parking" => Some(("parking", LABEL_CLASS_TRANSPORT)), "charging_station" => Some(("charger", LABEL_CLASS_TRANSPORT)), "place_of_worship" => Some(("place_of_worship", LABEL_CLASS_CULTURE)), "cinema" => Some(("cinema", LABEL_CLASS_CULTURE)), "theatre" => Some(("theatre", LABEL_CLASS_CULTURE)), "library" => Some(("library", LABEL_CLASS_CULTURE)), _ => None, }; } if let Some(tourism) = tags.get("tourism") { return match tourism.as_str() { "hotel" | "guest_house" | "hostel" => Some(("hotel", LABEL_CLASS_CULTURE)), "museum" | "gallery" => Some(("museum", LABEL_CLASS_CULTURE)), "information" => Some(("information", LABEL_CLASS_CULTURE)), _ => Some(("dot", LABEL_CLASS_CULTURE)), }; } if tags.contains_key("leisure") { return Some(("dot", LABEL_CLASS_GREEN)); } // Named POI with no matched symbol: carto's small colored dot, so the // place still shows up (and its label gets the class color). if tags.contains_key("name") && (tags.contains_key("amenity") || tags.contains_key("office") || tags.contains_key("craft")) { return Some(("dot", LABEL_CLASS_DEFAULT)); } None } #[cfg(test)] mod tests { use super::*; #[test] fn test_icon_size_px() { assert_eq!(ICON_SIZE_PX, 14.0); } #[test] fn test_icon_min_zoom() { assert_eq!(ICON_MIN_ZOOM, 17); } #[test] fn test_label_class_constants() { assert_eq!(LABEL_CLASS_DEFAULT, 0); assert_eq!(LABEL_CLASS_AMENITY, 1); assert_eq!(LABEL_CLASS_SHOP, 2); assert_eq!(LABEL_CLASS_TRANSPORT, 3); assert_eq!(LABEL_CLASS_CULTURE, 4); assert_eq!(LABEL_CLASS_GREEN, 5); assert_eq!(LABEL_CLASS_MUTED, 6); assert_eq!(LABEL_CLASS_TREE, 7); assert_eq!(LABEL_CLASS_HEALTH, 8); } #[test] fn test_icons_singleton() { let icons1 = icons(); let icons2 = icons(); // Should return the same static reference assert_eq!(icons1.len(), icons2.len()); assert!(!icons1.is_empty()); } #[test] fn test_icons_contains_common_icons() { let icons = icons(); assert!(icons.contains_key("restaurant")); assert!(icons.contains_key("cafe")); assert!(icons.contains_key("hotel")); assert!(icons.contains_key("bank")); assert!(icons.contains_key("pharmacy")); } #[test] fn test_icon_mesh_existing() { let mesh = icon_mesh("restaurant"); assert!(mesh.is_some()); let mesh = mesh.unwrap(); assert!(!mesh.verts.is_empty()); assert!(!mesh.indices.is_empty()); } #[test] fn test_icon_mesh_nonexistent() { let mesh = icon_mesh("nonexistent_icon_12345"); assert!(mesh.is_none()); } #[test] fn test_icon_mesh_all_common_icons() { let common_icons = vec![ "alcohol", "atm", "bakery", "bank", "bar", "beauty", "bicycle", "butcher", "cafe", "cinema", "clothes", "coffee", "convenience", "fast_food", "florist", "gift", "greengrocer", "hairdresser", "hotel", "ice_cream", "laundry", "library", "museum", "nightclub", "pharmacy", "place_of_worship", "pub", "restaurant", "supermarket", "theatre", "bench", "waste_basket", "recycling", "playground", "statue", "entrance", "information", "traffic_signals", "charger_pin", "parking", "charger" ]; for icon_name in common_icons { let mesh = icon_mesh(icon_name); assert!(mesh.is_some(), "Icon '{}' should exist", icon_name); let mesh = mesh.unwrap(); assert!(!mesh.verts.is_empty(), "Icon '{}' should have vertices", icon_name); assert!(!mesh.indices.is_empty(), "Icon '{}' should have indices", icon_name); } } #[test] fn test_micro_icon_for_tags_bench() { let mut tags = HashMap::new(); tags.insert("amenity".to_string(), "bench".to_string()); let result = micro_icon_for_tags(&tags); assert!(result.is_some()); let (icon_name, label_class) = result.unwrap(); assert_eq!(icon_name, "bench"); assert_eq!(label_class, LABEL_CLASS_MUTED); } #[test] fn test_micro_icon_for_tags_waste_basket() { let mut tags = HashMap::new(); tags.insert("amenity".to_string(), "waste_basket".to_string()); let result = micro_icon_for_tags(&tags); assert!(result.is_some()); let (icon_name, _) = result.unwrap(); assert_eq!(icon_name, "waste_basket"); } #[test] fn test_micro_icon_for_tags_tree() { let mut tags = HashMap::new(); tags.insert("natural".to_string(), "tree".to_string()); let result = micro_icon_for_tags(&tags); assert!(result.is_some()); let (icon_name, label_class) = result.unwrap(); assert_eq!(icon_name, "tree"); assert_eq!(label_class, LABEL_CLASS_TREE); } #[test] fn test_micro_icon_for_tags_no_match() { let mut tags = HashMap::new(); tags.insert("highway".to_string(), "residential".to_string()); let result = micro_icon_for_tags(&tags); assert!(result.is_none()); } #[test] fn test_micro_icon_for_tags_empty_tags() { let tags = HashMap::new(); let result = micro_icon_for_tags(&tags); assert!(result.is_none()); } #[test] fn test_icon_for_tags_restaurant() { let mut tags = HashMap::new(); tags.insert("amenity".to_string(), "restaurant".to_string()); let result = icon_for_tags(&tags); assert!(result.is_some()); let (icon_name, label_class) = result.unwrap(); assert_eq!(icon_name, "restaurant"); assert_eq!(label_class, LABEL_CLASS_AMENITY); } #[test] fn test_icon_for_tags_cafe() { let mut tags = HashMap::new(); tags.insert("amenity".to_string(), "cafe".to_string()); let result = icon_for_tags(&tags); assert!(result.is_some()); let (icon_name, _) = result.unwrap(); assert_eq!(icon_name, "cafe"); } #[test] fn test_icon_for_tags_hotel() { let mut tags = HashMap::new(); tags.insert("tourism".to_string(), "hotel".to_string()); let result = icon_for_tags(&tags); assert!(result.is_some()); let (icon_name, label_class) = result.unwrap(); assert_eq!(icon_name, "hotel"); assert_eq!(label_class, LABEL_CLASS_AMENITY); } #[test] fn test_icon_for_tags_bank() { let mut tags = HashMap::new(); tags.insert("amenity".to_string(), "bank".to_string()); let result = icon_for_tags(&tags); assert!(result.is_some()); let (icon_name, label_class) = result.unwrap(); assert_eq!(icon_name, "bank"); assert_eq!(label_class, LABEL_CLASS_SHOP); } #[test] fn test_icon_for_tags_pharmacy() { let mut tags = HashMap::new(); tags.insert("amenity".to_string(), "pharmacy".to_string()); let result = icon_for_tags(&tags); assert!(result.is_some()); let (icon_name, label_class) = result.unwrap(); assert_eq!(icon_name, "pharmacy"); assert_eq!(label_class, LABEL_CLASS_HEALTH); } #[test] fn test_icon_for_tags_supermarket() { let mut tags = HashMap::new(); tags.insert("shop".to_string(), "supermarket".to_string()); let result = icon_for_tags(&tags); assert!(result.is_some()); let (icon_name, label_class) = result.unwrap(); assert_eq!(icon_name, "supermarket"); assert_eq!(label_class, LABEL_CLASS_SHOP); } #[test] fn test_icon_for_tags_museum() { let mut tags = HashMap::new(); tags.insert("tourism".to_string(), "museum".to_string()); let result = icon_for_tags(&tags); assert!(result.is_some()); let (icon_name, label_class) = result.unwrap(); assert_eq!(icon_name, "museum"); assert_eq!(label_class, LABEL_CLASS_CULTURE); } #[test] fn test_icon_for_tags_park() { let mut tags = HashMap::new(); tags.insert("leisure".to_string(), "park".to_string()); let result = icon_for_tags(&tags); assert!(result.is_some()); let (_, label_class) = result.unwrap(); assert_eq!(label_class, LABEL_CLASS_GREEN); } #[test] fn test_icon_for_tags_no_match() { let mut tags = HashMap::new(); tags.insert("highway".to_string(), "residential".to_string()); let result = icon_for_tags(&tags); assert!(result.is_none()); } #[test] fn test_icon_for_tags_empty_tags() { let tags = HashMap::new(); let result = icon_for_tags(&tags); assert!(result.is_none()); } #[test] fn test_icon_for_tags_priority() { // If multiple tags match, should return the first match let mut tags = HashMap::new(); tags.insert("amenity".to_string(), "restaurant".to_string()); tags.insert("tourism".to_string(), "hotel".to_string()); let result = icon_for_tags(&tags); assert!(result.is_some()); // The function should return one of them (implementation-dependent) } #[test] fn test_transform_coord() { let result = transform_coord(10.0, 5.0, 2.0); assert!((result - 10.0).abs() < 0.001); // (10 - 5) * 2 = 10 } #[test] fn test_transform_coord_zero() { let result = transform_coord(0.0, 0.0, 1.0); assert!((result - 0.0).abs() < 0.001); } #[test] fn test_transform_coord_negative() { let result = transform_coord(-5.0, 0.0, 1.0); assert!((result - (-5.0)).abs() < 0.001); } #[test] fn test_build_icon_mesh_valid_svg() { let svg = r#""#; let mesh = build_icon_mesh(svg); assert!(mesh.is_some()); let mesh = mesh.unwrap(); assert!(!mesh.verts.is_empty()); assert!(!mesh.indices.is_empty()); } #[test] fn test_build_icon_mesh_invalid_svg() { let svg = "not valid svg"; let mesh = build_icon_mesh(svg); // Should return None for invalid SVG assert!(mesh.is_none()); } #[test] fn test_build_disc_mesh() { let mesh = build_disc_mesh(5.0); assert!(mesh.is_some()); let mesh = mesh.unwrap(); assert!(!mesh.verts.is_empty()); assert!(!mesh.indices.is_empty()); } #[test] fn test_build_disc_mesh_zero_radius() { let mesh = build_disc_mesh(0.0); // Should still create a mesh, even if degenerate assert!(mesh.is_some()); } #[test] fn test_icon_mesh_consistency() { // Getting the same icon twice should return the same mesh let mesh1 = icon_mesh("restaurant"); let mesh2 = icon_mesh("restaurant"); assert!(mesh1.is_some()); assert!(mesh2.is_some()); let mesh1 = mesh1.unwrap(); let mesh2 = mesh2.unwrap(); assert_eq!(mesh1.verts.len(), mesh2.verts.len()); assert_eq!(mesh1.indices.len(), mesh2.indices.len()); } #[test] fn test_micro_icon_for_tags_playground() { let mut tags = HashMap::new(); tags.insert("leisure".to_string(), "playground".to_string()); let result = micro_icon_for_tags(&tags); assert!(result.is_some()); let (icon_name, _) = result.unwrap(); assert_eq!(icon_name, "playground"); } #[test] fn test_icon_for_tags_charger() { let mut tags = HashMap::new(); tags.insert("amenity".to_string(), "charging_station".to_string()); let result = icon_for_tags(&tags); assert!(result.is_some()); let (icon_name, _) = result.unwrap(); assert_eq!(icon_name, "charger"); } }