This commit is contained in:
Dongho Kim
2025-12-19 02:24:05 +09:00
parent 1dcdce3ef1
commit 136723ca24
20 changed files with 1422 additions and 603 deletions

View File

@@ -11,3 +11,6 @@ anyhow = "1.0"
memmap2 = "0.9"
dotenv = "0.15"
earcutr = "0.4"
wgpu = "0.19"
bytemuck = { version = "1.14", features = ["derive"] }
pollster = "0.3"

View File

@@ -16,7 +16,8 @@ pub enum DbTask {
table: &'static str,
id: i64,
tags: HashMap<String, String>,
points: Vec<u8>,
points: Vec<u8>,
vertex_buffer: Vec<u8>,
x: i32,
y: i32
},

View File

@@ -38,6 +38,13 @@ async fn main() -> Result<()> {
// Truncate tables
scylla_repo.truncate_tables().await?;
// Initialize GPU mesh generation service
println!("Initializing GPU mesh generation service...");
let mesh_service = Arc::new(
pollster::block_on(services::mesh_service::MeshGenerationService::new())?
);
println!("Mesh service initialized!");
let path = std::env::var("OSM_PBF_PATH")
.or_else(|_| std::env::var("HOST_PBF_PATH"))
.unwrap_or_else(|_| "europe-latest.osm.pbf".to_string());
@@ -79,9 +86,9 @@ async fn main() -> Result<()> {
let _ = repo.insert_node(zoom, id, lat, lon, tags, x, y).await;
});
}
DbTask::Way { zoom, table, id, tags, points, x, y } => {
DbTask::Way { zoom, table, id, tags, points, vertex_buffer, x, y } => {
join_set.spawn(async move {
let _ = repo.insert_way(table, zoom, id, tags, points, x, y).await;
let _ = repo.insert_way(table, zoom, id, tags, points, vertex_buffer, x, y).await;
});
}
}
@@ -95,8 +102,10 @@ async fn main() -> Result<()> {
// Run the PBF reader in a blocking task
let tx_clone = tx.clone();
let mesh_service_clone = mesh_service.clone();
let reader_handle = tokio::task::spawn_blocking(move || -> Result<(usize, usize, usize)> {
let tx = tx_clone;
let mesh_svc = mesh_service_clone;
let mut node_count = 0;
let mut way_count = 0;
let mut relation_count = 0;
@@ -259,22 +268,140 @@ async fn main() -> Result<()> {
}
if is_highway || treat_as_water_line {
let task = DbTask::Way { zoom: zoom_i32, table: "ways", id, tags: tags.clone(), points: line_blob.clone(), x, y };
// Generate road geometry
let projected_points: Vec<[f32; 2]> = simplified_points.iter()
.map(|(lat, lon)| {
let (x, y) = GeometryService::project(*lat, *lon);
[x, y]
})
.collect();
let highway_tag = tags.get("highway").map(|s| s.as_str());
let road_type = match highway_tag.unwrap_or("") {
"motorway" | "motorway_link" | "trunk" | "trunk_link" => 0.0,
"primary" | "primary_link" => 1.0,
"secondary" | "secondary_link" => 2.0,
_ => 3.0,
};
let default_lanes: f32 = match highway_tag.unwrap_or("") {
"motorway" | "trunk" => 4.0,
"motorway_link" | "trunk_link" | "primary" => 2.0,
_ => 2.0,
};
let lanes: f32 = tags.get("lanes")
.and_then(|s| s.parse().ok())
.unwrap_or(default_lanes);
// DEBUG: Log first way to validate mesh generation
if way_count == 1 {
println!("DEBUG Way {}: {} projected points, generating mesh...", id, projected_points.len());
}
let vertex_buffer = if treat_as_water_line {
mesh_svc.generate_polygon_geometry(&projected_points)
} else {
mesh_svc.generate_road_geometry(&projected_points, lanes, road_type)
};
// DEBUG: Log buffer size
if way_count == 1 {
println!("DEBUG Way {}: vertex_buffer size = {} bytes", id, vertex_buffer.len());
}
let task = DbTask::Way {
zoom: zoom_i32,
table: "ways",
id,
tags: tags.clone(),
points: line_blob.clone(),
vertex_buffer,
x,
y
};
let _ = tx.blocking_send(task);
}
if treat_as_building {
let task = DbTask::Way { zoom: zoom_i32, table: "buildings", id, tags: tags.clone(), points: polygon_blob.clone(), x, y };
// Generate building mesh
let projected_points: Vec<[f32; 2]> = final_points.iter()
.map(|(lat, lon)| {
let (x, y) = GeometryService::project(*lat, *lon);
[x, y]
})
.collect();
let building_type = tags.get("building").map(|s| s.as_str()).unwrap_or("yes");
let color: [f32; 3] = match building_type {
"house" | "apartments" | "residential" | "detached" | "semidetached_house" | "terrace" | "dormitory" =>
[0.95, 0.94, 0.91],
"commercial" | "retail" | "office" | "supermarket" | "kiosk" | "hotel" =>
[0.91, 0.89, 0.86],
"industrial" | "warehouse" | "factory" | "manufacture" =>
[0.85, 0.84, 0.80],
_ => [0.85, 0.85, 0.85],
};
let vertex_buffer = mesh_svc.generate_building_geometry(&projected_points, color);
let task = DbTask::Way {
zoom: zoom_i32,
table: "buildings",
id,
tags: tags.clone(),
points: polygon_blob.clone(),
vertex_buffer,
x,
y
};
let _ = tx.blocking_send(task);
}
if treat_as_water_area {
let task = DbTask::Way { zoom: zoom_i32, table: "water", id, tags: tags.clone(), points: polygon_blob.clone(), x, y };
// Generate water polygon mesh
let projected_points: Vec<[f32; 2]> = final_points.iter()
.map(|(lat, lon)| {
let (x, y) = GeometryService::project(*lat, *lon);
[x, y]
})
.collect();
let vertex_buffer = mesh_svc.generate_polygon_geometry(&projected_points);
let task = DbTask::Way {
zoom: zoom_i32,
table: "water",
id,
tags: tags.clone(),
points: polygon_blob.clone(),
vertex_buffer,
x,
y
};
let _ = tx.blocking_send(task);
}
if treat_as_landuse {
let task = DbTask::Way { zoom: zoom_i32, table: "landuse", id, tags: tags.clone(), points: polygon_blob.clone(), x, y };
// Generate landuse polygon mesh
let projected_points: Vec<[f32; 2]> = final_points.iter()
.map(|(lat, lon)| {
let (x, y) = GeometryService::project(*lat, *lon);
[x, y]
})
.collect();
let vertex_buffer = mesh_svc.generate_polygon_geometry(&projected_points);
let task = DbTask::Way {
zoom: zoom_i32,
table: "landuse",
id,
tags: tags.clone(),
points: polygon_blob.clone(),
vertex_buffer,
x,
y
};
let _ = tx.blocking_send(task);
}
@@ -303,6 +430,20 @@ async fn main() -> Result<()> {
}
}
// Also extract line ref (e.g., S1, U4) from route relations
if let Some(line_ref) = tags.get("ref") {
// Only propagate S-Bahn/U-Bahn style refs (starts with S or U followed by digit)
if (line_ref.starts_with('S') || line_ref.starts_with('U')) && line_ref.len() >= 2 {
let member_count = rel.members().filter(|m| matches!(m.member_type, osmpbf::RelMemberType::Way)).count();
println!("DEBUG: Found transit line ref '{}' with {} way members", line_ref, member_count);
for member in rel.members() {
if let osmpbf::RelMemberType::Way = member.member_type {
railway_store.set_ref(member.member_id, line_ref.clone());
}
}
}
}
if tags.get("type").map(|t| t == "multipolygon").unwrap_or(false) {
let is_water = tags.get("natural").map(|v| v == "water" || v == "wetland" || v == "bay").unwrap_or(false) ||
tags.get("waterway").map(|v| v == "riverbank" || v == "river" || v == "canal").unwrap_or(false) ||
@@ -355,7 +496,27 @@ async fn main() -> Result<()> {
}
let table = if is_water { "water" } else { "landuse" };
let task = DbTask::Way { zoom: zoom_i32, table, id, tags: tags.clone(), points: polygon_blob.clone(), x, y };
// Generate polygon mesh for multipolygons
let projected_points: Vec<[f32; 2]> = final_points.iter()
.map(|(lat, lon)| {
let (x, y) = GeometryService::project(*lat, *lon);
[x, y]
})
.collect();
let vertex_buffer = mesh_svc.generate_polygon_geometry(&projected_points);
let task = DbTask::Way {
zoom: zoom_i32,
table,
id,
tags: tags.clone(),
points: polygon_blob.clone(),
vertex_buffer,
x,
y
};
let _ = tx.blocking_send(task);
}
}
@@ -372,14 +533,18 @@ async fn main() -> Result<()> {
}
})?;
let (railways, colors) = railway_store.into_data();
println!("Inserting {} railway ways with colors...", railways.len());
let (railways, colors, refs) = railway_store.into_data();
println!("Inserting {} railway ways with colors and line refs...", railways.len());
for (id, railway) in railways {
let mut tags = railway.tags;
if let Some(colour) = colors.get(&id) {
tags.insert("colour".to_string(), colour.clone());
}
if let Some(line_ref) = refs.get(&id) {
tags.insert("line_ref".to_string(), line_ref.clone());
}
// Insert for all applicable zoom levels
for &zoom in &FilteringService::ZOOM_LEVELS {
@@ -387,6 +552,41 @@ async fn main() -> Result<()> {
let (x, y) = TileService::lat_lon_to_tile(railway.first_lat, railway.first_lon, zoom);
let zoom_i32 = zoom as i32;
// Parse geometry from blob and generate railway mesh
let mut points: Vec<[f32; 2]> = Vec::new();
for chunk in railway.points.chunks(8) {
if chunk.len() < 8 { break; }
let lat = f32::from_le_bytes(chunk[0..4].try_into().unwrap_or([0u8; 4])) as f64;
let lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap_or([0u8; 4])) as f64;
let (x, y) = GeometryService::project(lat, lon);
points.push([x, y]);
}
// Parse color and rail type
let color_str = tags.get("colour").or(tags.get("color"));
let color = color_str
.map(|c| {
let c = c.trim_start_matches('#');
if c.len() == 6 {
let r = u8::from_str_radix(&c[0..2], 16).unwrap_or(0) as f32 / 255.0;
let g = u8::from_str_radix(&c[2..4], 16).unwrap_or(0) as f32 / 255.0;
let b = u8::from_str_radix(&c[4..6], 16).unwrap_or(0) as f32 / 255.0;
[r, g, b]
} else {
[0.0, 0.0, 0.0]
}
})
.unwrap_or([0.0, 0.0, 0.0]);
let rail_type_str = tags.get("railway").map(|s| s.as_str()).unwrap_or("rail");
let rail_type: f32 = match rail_type_str {
"subway" => 1.0,
"tram" => 2.0,
_ => 0.0,
};
let vertex_buffer = mesh_svc.generate_railway_geometry(&points, color, rail_type);
let task = DbTask::Way {
zoom: zoom_i32,
@@ -394,6 +594,7 @@ async fn main() -> Result<()> {
id,
tags: tags.clone(),
points: railway.points.clone(),
vertex_buffer,
x,
y
};

View File

@@ -12,6 +12,7 @@ pub struct RailwayWay {
pub struct RailwayStore {
ways: HashMap<i64, RailwayWay>, // way_id -> railway data
way_colors: HashMap<i64, String>, // way_id -> colour from route relation
way_refs: HashMap<i64, String>, // way_id -> ref (line name like S1, U3) from route relation
}
impl RailwayStore {
@@ -19,6 +20,7 @@ impl RailwayStore {
Self {
ways: HashMap::new(),
way_colors: HashMap::new(),
way_refs: HashMap::new(),
}
}
@@ -31,11 +33,20 @@ impl RailwayStore {
self.way_colors.entry(way_id).or_insert(color);
}
pub fn set_ref(&mut self, way_id: i64, line_ref: String) {
// Only set if not already set (first route relation wins)
self.way_refs.entry(way_id).or_insert(line_ref);
}
pub fn get_color(&self, way_id: i64) -> Option<&String> {
self.way_colors.get(&way_id)
}
pub fn get_ref(&self, way_id: i64) -> Option<&String> {
self.way_refs.get(&way_id)
}
pub fn into_data(self) -> (HashMap<i64, RailwayWay>, HashMap<i64, String>) {
(self.ways, self.way_colors)
pub fn into_data(self) -> (HashMap<i64, RailwayWay>, HashMap<i64, String>, HashMap<i64, String>) {
(self.ways, self.way_colors, self.way_refs)
}
}

View File

@@ -1,7 +1,6 @@
use scylla::{Session, SessionBuilder};
use std::sync::Arc;
use std::collections::HashMap;
use tokio::task::JoinSet;
use anyhow::Result;
pub struct ScyllaRepository {
@@ -33,19 +32,19 @@ impl ScyllaRepository {
// Create tables
session.query("CREATE TABLE IF NOT EXISTS map_data.nodes (zoom int, tile_x int, tile_y int, id bigint, lat double, lon double, tags map<text, text>, PRIMARY KEY ((zoom, tile_x, tile_y), id))", &[]).await?;
session.query("CREATE TABLE IF NOT EXISTS map_data.ways (zoom int, tile_x int, tile_y int, id bigint, tags map<text, text>, points blob, PRIMARY KEY ((zoom, tile_x, tile_y), id))", &[]).await?;
session.query("CREATE TABLE IF NOT EXISTS map_data.buildings (zoom int, tile_x int, tile_y int, id bigint, tags map<text, text>, points blob, PRIMARY KEY ((zoom, tile_x, tile_y), id))", &[]).await?;
session.query("CREATE TABLE IF NOT EXISTS map_data.water (zoom int, tile_x int, tile_y int, id bigint, tags map<text, text>, points blob, PRIMARY KEY ((zoom, tile_x, tile_y), id))", &[]).await?;
session.query("CREATE TABLE IF NOT EXISTS map_data.landuse (zoom int, tile_x int, tile_y int, id bigint, tags map<text, text>, points blob, PRIMARY KEY ((zoom, tile_x, tile_y), id))", &[]).await?;
session.query("CREATE TABLE IF NOT EXISTS map_data.railways (zoom int, tile_x int, tile_y int, id bigint, tags map<text, text>, points blob, PRIMARY KEY ((zoom, tile_x, tile_y), id))", &[]).await?;
session.query("CREATE TABLE IF NOT EXISTS map_data.ways (zoom int, tile_x int, tile_y int, id bigint, tags map<text, text>, points blob, vertex_buffer blob, PRIMARY KEY ((zoom, tile_x, tile_y), id))", &[]).await?;
session.query("CREATE TABLE IF NOT EXISTS map_data.buildings (zoom int, tile_x int, tile_y int, id bigint, tags map<text, text>, points blob, vertex_buffer blob, PRIMARY KEY ((zoom, tile_x, tile_y), id))", &[]).await?;
session.query("CREATE TABLE IF NOT EXISTS map_data.water (zoom int, tile_x int, tile_y int, id bigint, tags map<text, text>, points blob, vertex_buffer blob, PRIMARY KEY ((zoom, tile_x, tile_y), id))", &[]).await?;
session.query("CREATE TABLE IF NOT EXISTS map_data.landuse (zoom int, tile_x int, tile_y int, id bigint, tags map<text, text>, points blob, vertex_buffer blob, PRIMARY KEY ((zoom, tile_x, tile_y), id))", &[]).await?;
session.query("CREATE TABLE IF NOT EXISTS map_data.railways (zoom int, tile_x int, tile_y int, id bigint, tags map<text, text>, points blob, vertex_buffer blob, PRIMARY KEY ((zoom, tile_x, tile_y), id))", &[]).await?;
// Prepare statements
let insert_node = session.prepare("INSERT INTO map_data.nodes (zoom, tile_x, tile_y, id, lat, lon, tags) VALUES (?, ?, ?, ?, ?, ?, ?)").await?;
let insert_ways = session.prepare("INSERT INTO map_data.ways (zoom, tile_x, tile_y, id, tags, points) VALUES (?, ?, ?, ?, ?, ?)").await?;
let insert_buildings = session.prepare("INSERT INTO map_data.buildings (zoom, tile_x, tile_y, id, tags, points) VALUES (?, ?, ?, ?, ?, ?)").await?;
let insert_water = session.prepare("INSERT INTO map_data.water (zoom, tile_x, tile_y, id, tags, points) VALUES (?, ?, ?, ?, ?, ?)").await?;
let insert_landuse = session.prepare("INSERT INTO map_data.landuse (zoom, tile_x, tile_y, id, tags, points) VALUES (?, ?, ?, ?, ?, ?)").await?;
let insert_railways = session.prepare("INSERT INTO map_data.railways (zoom, tile_x, tile_y, id, tags, points) VALUES (?, ?, ?, ?, ?, ?)").await?;
let insert_ways = session.prepare("INSERT INTO map_data.ways (zoom, tile_x, tile_y, id, tags, points, vertex_buffer) VALUES (?, ?, ?, ?, ?, ?, ?)").await?;
let insert_buildings = session.prepare("INSERT INTO map_data.buildings (zoom, tile_x, tile_y, id, tags, points, vertex_buffer) VALUES (?, ?, ?, ?, ?, ?, ?)").await?;
let insert_water = session.prepare("INSERT INTO map_data.water (zoom, tile_x, tile_y, id, tags, points, vertex_buffer) VALUES (?, ?, ?, ?, ?, ?, ?)").await?;
let insert_landuse = session.prepare("INSERT INTO map_data.landuse (zoom, tile_x, tile_y, id, tags, points, vertex_buffer) VALUES (?, ?, ?, ?, ?, ?, ?)").await?;
let insert_railways = session.prepare("INSERT INTO map_data.railways (zoom, tile_x, tile_y, id, tags, points, vertex_buffer) VALUES (?, ?, ?, ?, ?, ?, ?)").await?;
Ok(Self {
session,
@@ -78,7 +77,7 @@ impl ScyllaRepository {
Ok(())
}
pub async fn insert_way(&self, table: &str, zoom: i32, id: i64, tags: HashMap<String, String>, points: Vec<u8>, x: i32, y: i32) -> Result<()> {
pub async fn insert_way(&self, table: &str, zoom: i32, id: i64, tags: HashMap<String, String>, points: Vec<u8>, vertex_buffer: Vec<u8>, x: i32, y: i32) -> Result<()> {
let statement = match table {
"ways" => &self.insert_ways,
"buildings" => &self.insert_buildings,
@@ -90,7 +89,7 @@ impl ScyllaRepository {
self.session.execute(
statement,
(zoom, x, y, id, tags, points),
(zoom, x, y, id, tags, points, vertex_buffer),
).await?;
Ok(())
}

View File

@@ -36,7 +36,7 @@ impl GeometryService {
if max_dist > epsilon {
let mut left = Self::simplify_points(&points[..=index], epsilon);
let mut right = Self::simplify_points(&points[index..], epsilon);
let right = Self::simplify_points(&points[index..], epsilon);
// Remove duplicate point at split
left.pop();
@@ -88,4 +88,18 @@ impl GeometryService {
result
}
/// Web Mercator Projection
/// Returns (x, y) in range [0.0, 1.0] for the whole world
pub fn project(lat: f64, lon: f64) -> (f32, f32) {
let x = (lon + 180.0) / 360.0;
let lat_rad = lat.to_radians();
let y = (1.0 - (lat_rad.tan() + (1.0 / lat_rad.cos())).ln() / std::f64::consts::PI) / 2.0;
// Validate results - clamp to valid range and handle NaN/Infinity
let x = if x.is_finite() { (x as f32).clamp(0.0, 1.0) } else { 0.5 };
let y = if y.is_finite() { (y as f32).clamp(0.0, 1.0) } else { 0.5 };
(x, y)
}
}

View File

@@ -0,0 +1,340 @@
use wgpu;
use bytemuck::{Pod, Zeroable};
use anyhow::Result;
/// Vertex format for road geometry (matches frontend RoadVertex)
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
pub struct RoadVertex {
pub center: [f32; 2],
pub normal: [f32; 2],
pub lanes: f32,
pub road_type: f32,
}
/// Vertex format for colored buildings (matches frontend ColoredVertex)
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
pub struct ColoredVertex {
pub position: [f32; 2],
pub color: [f32; 3],
pub _padding: f32, // Align to 16 bytes
}
/// Vertex format for simple polygons (matches frontend Vertex)
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
pub struct SimpleVertex {
pub position: [f32; 2],
}
/// Vertex format for railways (matches frontend RailwayVertex)
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
pub struct RailwayVertex {
pub center: [f32; 2],
pub normal: [f32; 2],
pub color: [f32; 3],
pub rail_type: f32,
}
/// GPU-based mesh generation service
/// Uses compute shaders to precompute geometry on the server
/// Falls back to CPU-only if GPU is not available
pub struct MeshGenerationService {
device: Option<wgpu::Device>,
queue: Option<wgpu::Queue>,
}
impl MeshGenerationService {
/// Initialize GPU device for headless compute
/// Falls back to CPU-only if GPU is unavailable
pub async fn new() -> Result<Self> {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(),
..Default::default()
});
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
compatible_surface: None,
})
.await;
match adapter {
Some(adapter) => {
match adapter.request_device(
&wgpu::DeviceDescriptor {
label: Some("Mesh Generation Device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
},
None,
).await {
Ok((device, queue)) => {
println!("GPU initialized: {:?}", adapter.get_info());
Ok(Self { device: Some(device), queue: Some(queue) })
}
Err(e) => {
println!("GPU device request failed: {}, falling back to CPU-only mode", e);
Ok(Self { device: None, queue: None })
}
}
}
None => {
println!("No GPU adapter found, using CPU-only mesh generation");
Ok(Self { device: None, queue: None })
}
}
}
/// Generate road geometry with miter joins (CPU implementation for now)
/// Returns vertex buffer as raw bytes ready for storage
pub fn generate_road_geometry(
&self,
points: &[[f32; 2]],
lanes: f32,
road_type: f32,
) -> Vec<u8> {
let vertices = Self::generate_road_mesh(points, lanes, road_type);
bytemuck::cast_slice(&vertices).to_vec()
}
/// Generate building geometry (already triangulated)
pub fn generate_building_geometry(
&self,
points: &[[f32; 2]],
color: [f32; 3],
) -> Vec<u8> {
let vertices: Vec<ColoredVertex> = points
.iter()
.map(|&position| ColoredVertex {
position,
color,
_padding: 0.0,
})
.collect();
bytemuck::cast_slice(&vertices).to_vec()
}
/// Generate simple polygon geometry (landuse, water)
pub fn generate_polygon_geometry(&self, points: &[[f32; 2]]) -> Vec<u8> {
let vertices: Vec<SimpleVertex> = points
.iter()
.map(|&position| SimpleVertex { position })
.collect();
bytemuck::cast_slice(&vertices).to_vec()
}
/// Generate railway geometry with color
pub fn generate_railway_geometry(
&self,
points: &[[f32; 2]],
color: [f32; 3],
rail_type: f32,
) -> Vec<u8> {
let vertices = Self::generate_railway_mesh(points, color, rail_type);
bytemuck::cast_slice(&vertices).to_vec()
}
// ========================================================================
// CPU-based mesh generation (port from frontend)
// TODO: Replace with GPU compute shaders for better performance
// ========================================================================
fn normalize(v: [f32; 2]) -> [f32; 2] {
let len = (v[0] * v[0] + v[1] * v[1]).sqrt();
if len < 0.00001 {
[0.0, 0.0]
} else {
[v[0] / len, v[1] / len]
}
}
fn dot(a: [f32; 2], b: [f32; 2]) -> f32 {
a[0] * b[0] + a[1] * b[1]
}
fn generate_road_mesh(points: &[[f32; 2]], lanes: f32, road_type: f32) -> Vec<RoadVertex> {
if points.len() < 2 {
return Vec::new();
}
// Compute normals for each segment
let mut segment_normals = Vec::with_capacity(points.len() - 1);
for i in 0..points.len() - 1 {
let p1 = points[i];
let p2 = points[i + 1];
let dx = p2[0] - p1[0];
let dy = p2[1] - p1[1];
let len = (dx * dx + dy * dy).sqrt();
if len < 0.000001 {
segment_normals.push([0.0, 0.0]);
} else {
segment_normals.push([-dy / len, dx / len]);
}
}
// Generate vertex pairs with miter joins
let mut point_pairs = Vec::with_capacity(points.len() * 2);
for i in 0..points.len() {
let normal: [f32; 2];
let mut miter_len = 1.0f32;
if i == 0 {
normal = segment_normals[0];
} else if i == points.len() - 1 {
normal = segment_normals[i - 1];
} else {
let n1 = segment_normals[i - 1];
let n2 = segment_normals[i];
if Self::dot(n1, n1) == 0.0 {
normal = n2;
} else if Self::dot(n2, n2) == 0.0 {
normal = n1;
} else {
let sum = [n1[0] + n2[0], n1[1] + n2[1]];
let miter = Self::normalize(sum);
let d = Self::dot(miter, n1);
if d.abs() < 0.1 {
normal = n1;
} else {
miter_len = 1.0 / d;
if miter_len > 4.0 {
miter_len = 4.0;
}
normal = miter;
}
}
}
let p = points[i];
let nx = normal[0] * miter_len;
let ny = normal[1] * miter_len;
point_pairs.push(RoadVertex {
center: p,
normal: [nx, ny],
lanes,
road_type,
});
point_pairs.push(RoadVertex {
center: p,
normal: [-nx, -ny],
lanes,
road_type,
});
}
// Triangulate
let mut triangle_vertices = Vec::with_capacity((points.len() - 1) * 6);
for i in 0..points.len() - 1 {
if Self::dot(segment_normals[i], segment_normals[i]) == 0.0 {
continue;
}
let i_base = 2 * i;
let j_base = 2 * (i + 1);
let v1 = point_pairs[i_base];
let v2 = point_pairs[i_base + 1];
let v3 = point_pairs[j_base];
let v4 = point_pairs[j_base + 1];
triangle_vertices.push(v1);
triangle_vertices.push(v2);
triangle_vertices.push(v3);
triangle_vertices.push(v2);
triangle_vertices.push(v4);
triangle_vertices.push(v3);
}
triangle_vertices
}
fn generate_railway_mesh(
points: &[[f32; 2]],
color: [f32; 3],
rail_type: f32,
) -> Vec<RailwayVertex> {
if points.len() < 2 {
return Vec::new();
}
// Similar to road mesh but for railways
let mut segment_normals = Vec::with_capacity(points.len() - 1);
for i in 0..points.len() - 1 {
let p1 = points[i];
let p2 = points[i + 1];
let dx = p2[0] - p1[0];
let dy = p2[1] - p1[1];
let len = (dx * dx + dy * dy).sqrt();
if len < 0.000001 {
segment_normals.push([0.0, 0.0]);
} else {
segment_normals.push([-dy / len, dx / len]);
}
}
let mut point_pairs = Vec::with_capacity(points.len() * 2);
for i in 0..points.len() {
let normal = if i == 0 {
segment_normals[0]
} else if i == points.len() - 1 {
segment_normals[i - 1]
} else {
let n1 = segment_normals[i - 1];
let n2 = segment_normals[i];
let sum = [n1[0] + n2[0], n1[1] + n2[1]];
Self::normalize(sum)
};
let p = points[i];
point_pairs.push(RailwayVertex {
center: p,
normal,
color,
rail_type,
});
point_pairs.push(RailwayVertex {
center: p,
normal: [-normal[0], -normal[1]],
color,
rail_type,
});
}
let mut triangle_vertices = Vec::with_capacity((points.len() - 1) * 6);
for i in 0..points.len() - 1 {
if Self::dot(segment_normals[i], segment_normals[i]) == 0.0 {
continue;
}
let i_base = 2 * i;
let j_base = 2 * (i + 1);
let v1 = point_pairs[i_base];
let v2 = point_pairs[i_base + 1];
let v3 = point_pairs[j_base];
let v4 = point_pairs[j_base + 1];
triangle_vertices.push(v1);
triangle_vertices.push(v2);
triangle_vertices.push(v3);
triangle_vertices.push(v2);
triangle_vertices.push(v4);
triangle_vertices.push(v3);
}
triangle_vertices
}
}

View File

@@ -3,4 +3,5 @@ pub mod multipolygon_service;
pub mod filtering_service;
pub mod tile_service;
pub mod railway_service;
pub mod mesh_service;