udpate sick
This commit is contained in:
@@ -12,6 +12,7 @@ bincode = "1.3"
|
|||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tower = "0.4"
|
tower = "0.4"
|
||||||
tower-http = { version = "0.5", features = ["cors", "fs", "compression-full"] }
|
tower-http = { version = "0.5", features = ["cors", "fs", "compression-full"] }
|
||||||
|
redis = { version = "0.24", features = ["tokio-comp", "connection-manager"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ use axum::{
|
|||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use crate::services::tile_service::TileService;
|
use crate::services::tile_service::TileService;
|
||||||
|
use crate::repositories::redis_repository::RedisRepository;
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub tile_service: Arc<TileService>,
|
pub tile_service: Arc<TileService>,
|
||||||
|
pub redis_repo: Arc<RedisRepository>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_tile(
|
pub async fn get_tile(
|
||||||
@@ -110,7 +112,24 @@ pub async fn get_tile_all(
|
|||||||
Path((z, x, y)): Path<(i32, i32, i32)>,
|
Path((z, x, y)): Path<(i32, i32, i32)>,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// Parallel fetching for performance
|
// Cache key for this tile
|
||||||
|
let cache_key = format!("tile:{}:{}:{}", z, x, y);
|
||||||
|
|
||||||
|
// Try to get from cache first
|
||||||
|
if let Ok(Some(cached_bytes)) = state.redis_repo.get(&cache_key).await {
|
||||||
|
tracing::debug!("Cache HIT for tile {}/{}/{}", z, x, y);
|
||||||
|
return (
|
||||||
|
[
|
||||||
|
(header::CONTENT_TYPE, "application/octet-stream"),
|
||||||
|
(header::CACHE_CONTROL, "public, max-age=86400"), // 24 hours
|
||||||
|
],
|
||||||
|
cached_bytes
|
||||||
|
).into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::debug!("Cache MISS for tile {}/{}/{}", z, x, y);
|
||||||
|
|
||||||
|
// Cache miss - fetch from database
|
||||||
let (nodes, ways, buildings, landuse, water, railways) = tokio::join!(
|
let (nodes, ways, buildings, landuse, water, railways) = tokio::join!(
|
||||||
state.tile_service.get_nodes(z, x, y),
|
state.tile_service.get_nodes(z, x, y),
|
||||||
state.tile_service.get_ways(z, x, y),
|
state.tile_service.get_ways(z, x, y),
|
||||||
@@ -120,23 +139,11 @@ pub async fn get_tile_all(
|
|||||||
state.tile_service.get_railways(z, x, y),
|
state.tile_service.get_railways(z, x, y),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Initial capacity estimaton (removed unused var)
|
if let (Ok(n), Ok(w), Ok(b), Ok(l), Ok(wt), Ok(r)) = (nodes.as_ref(), ways.as_ref(), buildings.as_ref(), landuse.as_ref(), water.as_ref(), railways.as_ref()) {
|
||||||
|
tracing::debug!("Tile {}/{}/{}: nodes={}, ways={}, buildings={}, landuse={}, water={}, railways={}",
|
||||||
|
z, x, y, n.len(), w.len(), b.len(), l.len(), wt.len(), r.len());
|
||||||
|
}
|
||||||
|
|
||||||
// Check errors and separate results?
|
|
||||||
// For now, the endpoint likely expects a single binary blob of combined types or just simple sequential data.
|
|
||||||
// The original logic didn't seem to implement get_tile_all in the viewed main.rs snippet.
|
|
||||||
// Based on standard practices, I'll return a struct or just concatenation if that's what the frontend expects.
|
|
||||||
// Wait, the original main.rs HAD `get_tile_all` registered but the implementation was truncated in view.
|
|
||||||
// I will implementation it by combining all into a single structured response or just separate vectors if I define a TileData DTO.
|
|
||||||
// Checking the plan... "models/tile_response.rs". I haven't created that yet.
|
|
||||||
// For now, I'll stick to individual endpoints as primary, but `get_tile_all` is useful.
|
|
||||||
// I'll return a tuple or struct serialized.
|
|
||||||
|
|
||||||
// Let's assume a structure similar to what the frontend expects.
|
|
||||||
// If I don't know the exact format of `get_tile_all` from previous code, I should look at it or just stub it safely.
|
|
||||||
// Actually, looking at `frontend/src/lib.rs` might reveal what it expects.
|
|
||||||
|
|
||||||
// For simplicity in this step, I will implement it returning a generic error if fails, or a tuple.
|
|
||||||
if let (Ok(n), Ok(w), Ok(b), Ok(l), Ok(wt), Ok(r)) = (nodes, ways, buildings, landuse, water, railways) {
|
if let (Ok(n), Ok(w), Ok(b), Ok(l), Ok(wt), Ok(r)) = (nodes, ways, buildings, landuse, water, railways) {
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
struct TileData {
|
struct TileData {
|
||||||
@@ -156,8 +163,26 @@ pub async fn get_tile_all(
|
|||||||
water: wt,
|
water: wt,
|
||||||
railways: r,
|
railways: r,
|
||||||
};
|
};
|
||||||
|
|
||||||
let bytes = bincode::serialize(&data).unwrap();
|
let bytes = bincode::serialize(&data).unwrap();
|
||||||
([(header::CONTENT_TYPE, "application/octet-stream")], bytes).into_response()
|
|
||||||
|
// Store in cache asynchronously (fire and forget)
|
||||||
|
let redis_clone = state.redis_repo.clone();
|
||||||
|
let cache_key_clone = cache_key.clone();
|
||||||
|
let bytes_clone = bytes.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = redis_clone.set(&cache_key_clone, &bytes_clone, 86400).await {
|
||||||
|
tracing::warn!("Failed to cache tile {}: {}", cache_key_clone, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(
|
||||||
|
[
|
||||||
|
(header::CONTENT_TYPE, "application/octet-stream"),
|
||||||
|
(header::CACHE_CONTROL, "public, max-age=86400"), // 24 hours
|
||||||
|
],
|
||||||
|
bytes
|
||||||
|
).into_response()
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
@@ -165,3 +190,4 @@ pub async fn get_tile_all(
|
|||||||
).into_response()
|
).into_response()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ pub async fn initialize_schema(session: &Session) -> Result<(), Box<dyn std::err
|
|||||||
id bigint,
|
id bigint,
|
||||||
tags map<text, text>,
|
tags map<text, text>,
|
||||||
points blob,
|
points blob,
|
||||||
|
vertex_buffer blob,
|
||||||
PRIMARY KEY ((zoom, tile_x, tile_y), id)
|
PRIMARY KEY ((zoom, tile_x, tile_y), id)
|
||||||
)",
|
)",
|
||||||
&[],
|
&[],
|
||||||
@@ -52,6 +53,7 @@ pub async fn initialize_schema(session: &Session) -> Result<(), Box<dyn std::err
|
|||||||
id bigint,
|
id bigint,
|
||||||
tags map<text, text>,
|
tags map<text, text>,
|
||||||
points blob,
|
points blob,
|
||||||
|
vertex_buffer blob,
|
||||||
PRIMARY KEY ((zoom, tile_x, tile_y), id)
|
PRIMARY KEY ((zoom, tile_x, tile_y), id)
|
||||||
)",
|
)",
|
||||||
&[],
|
&[],
|
||||||
@@ -67,6 +69,7 @@ pub async fn initialize_schema(session: &Session) -> Result<(), Box<dyn std::err
|
|||||||
id bigint,
|
id bigint,
|
||||||
tags map<text, text>,
|
tags map<text, text>,
|
||||||
points blob,
|
points blob,
|
||||||
|
vertex_buffer blob,
|
||||||
PRIMARY KEY ((zoom, tile_x, tile_y), id)
|
PRIMARY KEY ((zoom, tile_x, tile_y), id)
|
||||||
)",
|
)",
|
||||||
&[],
|
&[],
|
||||||
@@ -82,6 +85,7 @@ pub async fn initialize_schema(session: &Session) -> Result<(), Box<dyn std::err
|
|||||||
id bigint,
|
id bigint,
|
||||||
tags map<text, text>,
|
tags map<text, text>,
|
||||||
points blob,
|
points blob,
|
||||||
|
vertex_buffer blob,
|
||||||
PRIMARY KEY ((zoom, tile_x, tile_y), id)
|
PRIMARY KEY ((zoom, tile_x, tile_y), id)
|
||||||
)",
|
)",
|
||||||
&[],
|
&[],
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use tower_http::compression::CompressionLayer;
|
|||||||
|
|
||||||
use crate::repositories::way_repository::WayRepository;
|
use crate::repositories::way_repository::WayRepository;
|
||||||
use crate::repositories::node_repository::NodeRepository;
|
use crate::repositories::node_repository::NodeRepository;
|
||||||
|
use crate::repositories::redis_repository::RedisRepository;
|
||||||
use crate::services::tile_service::TileService;
|
use crate::services::tile_service::TileService;
|
||||||
use crate::api::handlers::tiles::{
|
use crate::api::handlers::tiles::{
|
||||||
get_tile, get_tile_ways, get_tile_buildings,
|
get_tile, get_tile_ways, get_tile_buildings,
|
||||||
@@ -37,20 +38,28 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
.build()
|
.build()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Initialize schema and seed data (Keep existing db module for now)
|
// Initialize schema and seed data
|
||||||
db::initialize_schema(&session).await?;
|
db::initialize_schema(&session).await?;
|
||||||
db::seed_data(&session).await?;
|
db::seed_data(&session).await?;
|
||||||
|
|
||||||
let session_arc = Arc::new(session);
|
let session_arc = Arc::new(session);
|
||||||
println!("Connected to ScyllaDB!");
|
println!("Connected to ScyllaDB!");
|
||||||
|
|
||||||
|
// Connect to Redis
|
||||||
|
println!("Connecting to Redis...");
|
||||||
|
let redis_uri = std::env::var("REDIS_URI")
|
||||||
|
.unwrap_or_else(|_| "redis://redis:6379".to_string());
|
||||||
|
let redis_repo = Arc::new(RedisRepository::new(&redis_uri).await?);
|
||||||
|
println!("Connected to Redis!");
|
||||||
|
|
||||||
// Dependency Injection
|
// Dependency Injection
|
||||||
let node_repo = Arc::new(NodeRepository::new(session_arc.clone()));
|
let node_repo = Arc::new(NodeRepository::new(session_arc.clone()));
|
||||||
let way_repo = Arc::new(WayRepository::new(session_arc.clone()));
|
let way_repo = Arc::new(WayRepository::new(session_arc.clone()));
|
||||||
let tile_service = Arc::new(TileService::new(node_repo, way_repo));
|
let tile_service = Arc::new(TileService::new(node_repo, way_repo));
|
||||||
|
|
||||||
let state = Arc::new(AppState {
|
let state = Arc::new(AppState {
|
||||||
tile_service: tile_service,
|
tile_service,
|
||||||
|
redis_repo,
|
||||||
});
|
});
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
pub mod way_repository;
|
pub mod way_repository;
|
||||||
pub mod node_repository;
|
pub mod node_repository;
|
||||||
|
pub mod redis_repository;
|
||||||
|
|||||||
43
backend/src/repositories/redis_repository.rs
Normal file
43
backend/src/repositories/redis_repository.rs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
use redis::{AsyncCommands, Client};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub struct RedisRepository {
|
||||||
|
client: Arc<Client>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RedisRepository {
|
||||||
|
pub async fn new(uri: &str) -> Result<Self, redis::RedisError> {
|
||||||
|
let client = Client::open(uri)?;
|
||||||
|
// Test connection
|
||||||
|
let mut conn = client.get_async_connection().await?;
|
||||||
|
let _: () = redis::cmd("PING").query_async(&mut conn).await?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
client: Arc::new(client),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get cached data by key
|
||||||
|
pub async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, redis::RedisError> {
|
||||||
|
let mut conn = self.client.get_async_connection().await?;
|
||||||
|
conn.get(key).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set data with TTL (time-to-live) in seconds
|
||||||
|
pub async fn set(&self, key: &str, value: &[u8], ttl_seconds: u64) -> Result<(), redis::RedisError> {
|
||||||
|
let mut conn = self.client.get_async_connection().await?;
|
||||||
|
conn.set_ex(key, value, ttl_seconds).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if key exists
|
||||||
|
pub async fn exists(&self, key: &str) -> Result<bool, redis::RedisError> {
|
||||||
|
let mut conn = self.client.get_async_connection().await?;
|
||||||
|
conn.exists(key).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a key
|
||||||
|
pub async fn delete(&self, key: &str) -> Result<(), redis::RedisError> {
|
||||||
|
let mut conn = self.client.get_async_connection().await?;
|
||||||
|
conn.del(key).await
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,18 @@ services:
|
|||||||
command: --smp 1 --memory 2G --overprovisioned 1 --api-address 0.0.0.0 --max-memory-for-unlimited-query-soft-limit 1073741824 --tombstone-warn-threshold 10000000
|
command: --smp 1 --memory 2G --overprovisioned 1 --api-address 0.0.0.0 --max-memory-for-unlimited-query-soft-limit 1073741824 --tombstone-warn-threshold 10000000
|
||||||
volumes:
|
volumes:
|
||||||
- scylla_data:/var/lib/scylla
|
- scylla_data:/var/lib/scylla
|
||||||
|
networks:
|
||||||
|
- maps-net
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: map-redis
|
||||||
|
command: redis-server --maxmemory 2gb --maxmemory-policy allkeys-lru
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
networks:
|
||||||
|
- maps-net
|
||||||
|
restart: always
|
||||||
|
|
||||||
app:
|
app:
|
||||||
build:
|
build:
|
||||||
@@ -15,6 +27,11 @@ services:
|
|||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
depends_on:
|
depends_on:
|
||||||
- scylla
|
- scylla
|
||||||
|
- redis
|
||||||
|
environment:
|
||||||
|
- REDIS_URI=redis://redis:6379
|
||||||
|
networks:
|
||||||
|
- maps-net
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
importer:
|
importer:
|
||||||
@@ -29,10 +46,19 @@ services:
|
|||||||
- SCYLLA_URI=scylla:9042
|
- SCYLLA_URI=scylla:9042
|
||||||
- OSM_PBF_PATH=/app/data.osm.pbf
|
- OSM_PBF_PATH=/app/data.osm.pbf
|
||||||
- CACHE_DIR=/cache
|
- CACHE_DIR=/cache
|
||||||
|
- DEBUG_WAY_ID=99
|
||||||
|
- VERBOSE_DEBUG=1
|
||||||
depends_on:
|
depends_on:
|
||||||
- scylla
|
- scylla
|
||||||
|
networks:
|
||||||
|
- maps-net
|
||||||
profiles:
|
profiles:
|
||||||
- import
|
- import
|
||||||
|
|
||||||
|
networks:
|
||||||
|
maps-net:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
scylla_data:
|
scylla_data:
|
||||||
|
redis_data:
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ web-sys = { version = "0.3", features = [
|
|||||||
"DomTokenList",
|
"DomTokenList",
|
||||||
"CssStyleDeclaration",
|
"CssStyleDeclaration",
|
||||||
"Performance",
|
"Performance",
|
||||||
|
"CanvasRenderingContext2d",
|
||||||
] }
|
] }
|
||||||
wgpu = { version = "0.19", default-features = false, features = ["webgl", "wgsl"] }
|
wgpu = { version = "0.19", default-features = false, features = ["webgl", "wgsl"] }
|
||||||
winit = { version = "0.29", default-features = false, features = ["rwh_06"] }
|
winit = { version = "0.29", default-features = false, features = ["rwh_06"] }
|
||||||
|
|||||||
@@ -520,6 +520,18 @@
|
|||||||
<div class="compass-center"></div>
|
<div class="compass-center"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Canvas for GPU-based label rendering (Apple Maps approach) -->
|
||||||
|
<canvas id="label-canvas" style="
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
background: transparent;
|
||||||
|
"></canvas>
|
||||||
|
|
||||||
<div id="labels"></div>
|
<div id="labels"></div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -255,8 +255,8 @@ pub fn extract_labels(tile_data: &TileData) -> Vec<CachedLabel> {
|
|||||||
} else {
|
} else {
|
||||||
"rail"
|
"rail"
|
||||||
};
|
};
|
||||||
// Log for debugging
|
// Debug logging removed for production performance
|
||||||
web_sys::console::log_1(&format!("Transit label found: {} at ({}, {})", line_ref, mid_point[0], mid_point[1]).into());
|
// web_sys::console::log_1(&format!("Transit label found: {} at ({}, {})", line_ref, mid_point[0], mid_point[1]).into());
|
||||||
|
|
||||||
candidates.push(CachedLabel {
|
candidates.push(CachedLabel {
|
||||||
name: line_ref.to_string(),
|
name: line_ref.to_string(),
|
||||||
@@ -273,7 +273,7 @@ pub fn extract_labels(tile_data: &TileData) -> Vec<CachedLabel> {
|
|||||||
candidates
|
candidates
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update DOM labels using cached data - much faster than processing raw data each frame
|
/// Update labels using Canvas API (GPU-accelerated) - Apple Maps approach
|
||||||
pub fn update_labels(
|
pub fn update_labels(
|
||||||
window: &web_sys::Window,
|
window: &web_sys::Window,
|
||||||
camera: &Camera,
|
camera: &Camera,
|
||||||
@@ -283,8 +283,45 @@ pub fn update_labels(
|
|||||||
_scale_factor: f64,
|
_scale_factor: f64,
|
||||||
) {
|
) {
|
||||||
let document = window.document().unwrap();
|
let document = window.document().unwrap();
|
||||||
let container = document.get_element_by_id("labels").unwrap();
|
|
||||||
container.set_inner_html("");
|
// Get canvas and 2D context
|
||||||
|
let canvas = document
|
||||||
|
.get_element_by_id("label-canvas")
|
||||||
|
.unwrap()
|
||||||
|
.dyn_into::<web_sys::HtmlCanvasElement>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let ctx = canvas
|
||||||
|
.get_context("2d")
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.dyn_into::<web_sys::CanvasRenderingContext2d>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Handle high-DPI displays - only resize if dimensions changed
|
||||||
|
let dpr = window.device_pixel_ratio();
|
||||||
|
let canvas_width = (width * dpr) as u32;
|
||||||
|
let canvas_height = (height * dpr) as u32;
|
||||||
|
|
||||||
|
// Only resize if dimensions changed to prevent flickering
|
||||||
|
if canvas.width() != canvas_width || canvas.height() != canvas_height {
|
||||||
|
canvas.set_width(canvas_width);
|
||||||
|
canvas.set_height(canvas_height);
|
||||||
|
|
||||||
|
// Set CSS size
|
||||||
|
let canvas_html: web_sys::HtmlElement = canvas.clone().dyn_into().unwrap();
|
||||||
|
let style = canvas_html.style();
|
||||||
|
let _ = style.set_property("width", &format!("{}px", width));
|
||||||
|
let _ = style.set_property("height", &format!("{}px", height));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset transform and clear
|
||||||
|
ctx.set_transform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0).unwrap();
|
||||||
|
ctx.clear_rect(0.0, 0.0, canvas_width as f64, canvas_height as f64);
|
||||||
|
|
||||||
|
// Apply DPI scaling
|
||||||
|
let _ = ctx.scale(dpr, dpr);
|
||||||
|
|
||||||
|
|
||||||
let visible_tiles = TileService::get_visible_tiles(camera);
|
let visible_tiles = TileService::get_visible_tiles(camera);
|
||||||
let is_dark = document.document_element()
|
let is_dark = document.document_element()
|
||||||
@@ -307,7 +344,63 @@ pub fn update_labels(
|
|||||||
if label.label_type == LabelType::Transit && !show_transit {
|
if label.label_type == LabelType::Transit && !show_transit {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (zoom as f64) > label.min_zoom {
|
|
||||||
|
// Zoom range filtering - each label type has min and max zoom
|
||||||
|
let (min_zoom, max_zoom) = match label.label_type {
|
||||||
|
LabelType::Country => (0.0, 1000.0),
|
||||||
|
LabelType::City => {
|
||||||
|
// Cities have different ranges based on their min_zoom (which indicates size)
|
||||||
|
if label.min_zoom < 100.0 {
|
||||||
|
// Large cities (city/capital)
|
||||||
|
(20.0, 50000.0)
|
||||||
|
} else if label.min_zoom < 1000.0 {
|
||||||
|
// Towns
|
||||||
|
(500.0, 100000.0)
|
||||||
|
} else if label.min_zoom < 3000.0 {
|
||||||
|
// Villages
|
||||||
|
(2000.0, 200000.0)
|
||||||
|
} else {
|
||||||
|
// Hamlets/suburbs
|
||||||
|
(4000.0, 300000.0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
LabelType::Street => {
|
||||||
|
// Streets should only show in a specific zoom range
|
||||||
|
if label.min_zoom < 10000.0 {
|
||||||
|
// Motorways/trunk
|
||||||
|
(5000.0, 150000.0)
|
||||||
|
} else if label.min_zoom < 30000.0 {
|
||||||
|
// Primary
|
||||||
|
(20000.0, 200000.0)
|
||||||
|
} else if label.min_zoom < 80000.0 {
|
||||||
|
// Secondary
|
||||||
|
(50000.0, 300000.0)
|
||||||
|
} else if label.min_zoom < 150000.0 {
|
||||||
|
// Tertiary
|
||||||
|
(100000.0, 500000.0)
|
||||||
|
} else {
|
||||||
|
// Residential
|
||||||
|
(300000.0, 1000000.0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
LabelType::Poi => {
|
||||||
|
// POIs have different ranges based on importance
|
||||||
|
if label.min_zoom < 1000.0 {
|
||||||
|
// Major POIs (attractions, museums)
|
||||||
|
(500.0, 100000.0)
|
||||||
|
} else if label.min_zoom < 3000.0 {
|
||||||
|
// Medium POIs (hotels, hospitals)
|
||||||
|
(2000.0, 200000.0)
|
||||||
|
} else {
|
||||||
|
// Minor POIs (restaurants, cafes)
|
||||||
|
(5000.0, 300000.0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
LabelType::Transit => (100.0, 50000.0),
|
||||||
|
};
|
||||||
|
|
||||||
|
let zoom_f64 = zoom as f64;
|
||||||
|
if zoom_f64 >= min_zoom && zoom_f64 <= max_zoom {
|
||||||
render_list.push(label);
|
render_list.push(label);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -317,21 +410,22 @@ pub fn update_labels(
|
|||||||
// Sort by priority (high to low)
|
// Sort by priority (high to low)
|
||||||
render_list.sort_by(|a, b| b.priority.cmp(&a.priority));
|
render_list.sort_by(|a, b| b.priority.cmp(&a.priority));
|
||||||
|
|
||||||
// Zoom-based label limit to prevent DOM thrashing at low zoom levels
|
// Zoom-based label limit to prevent performance issues
|
||||||
let max_labels = if zoom < 100.0 {
|
let max_labels = if zoom < 100.0 {
|
||||||
50 // World/continent view - only show major features
|
20
|
||||||
} else if zoom < 1000.0 {
|
} else if zoom < 1000.0 {
|
||||||
200 // Country/region view - show cities and major POIs
|
100
|
||||||
|
} else if zoom < 5000.0 {
|
||||||
|
300
|
||||||
} else {
|
} else {
|
||||||
usize::MAX // City view and closer - show all labels
|
500
|
||||||
};
|
};
|
||||||
|
|
||||||
// Truncate to limit
|
|
||||||
if render_list.len() > max_labels {
|
if render_list.len() > max_labels {
|
||||||
render_list.truncate(max_labels);
|
render_list.truncate(max_labels);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collision detection and placement
|
// Collision detection
|
||||||
let mut placed_rects: Vec<(f64, f64, f64, f64)> = Vec::new();
|
let mut placed_rects: Vec<(f64, f64, f64, f64)> = Vec::new();
|
||||||
|
|
||||||
for label in render_list {
|
for label in render_list {
|
||||||
@@ -340,20 +434,40 @@ pub fn update_labels(
|
|||||||
let cx = x * uniforms.params[0] + uniforms.params[2];
|
let cx = x * uniforms.params[0] + uniforms.params[2];
|
||||||
let cy = y * uniforms.params[1] + uniforms.params[3];
|
let cy = y * uniforms.params[1] + uniforms.params[3];
|
||||||
|
|
||||||
// Tighter clip check - skip labels clearly off-screen
|
// Clip check
|
||||||
if cx < -1.0 || cx > 1.0 || cy < -1.0 || cy > 1.0 { continue; }
|
if cx < -1.0 || cx > 1.0 || cy < -1.0 || cy > 1.0 { continue; }
|
||||||
|
|
||||||
// Convert to CSS pixels
|
// Convert to CSS pixels
|
||||||
let css_x = (cx as f64 + 1.0) * 0.5 * client_width;
|
let css_x = (cx as f64 + 1.0) * 0.5 * client_width;
|
||||||
let css_y = (1.0 - cy as f64) * 0.5 * client_height;
|
let css_y = (1.0 - cy as f64) * 0.5 * client_height;
|
||||||
|
|
||||||
// Estimate dimensions
|
// Set font and style based on label type
|
||||||
let (est_w, est_h) = match label.label_type {
|
let (font, color, est_w, est_h) = match label.label_type {
|
||||||
LabelType::Country => (label.name.len() as f64 * 12.0 + 20.0, 24.0),
|
LabelType::Country => {
|
||||||
LabelType::City => (label.name.len() as f64 * 8.0 + 10.0, 16.0),
|
let font = "bold 24px Inter, -apple-system, sans-serif";
|
||||||
LabelType::Street => (label.name.len() as f64 * 6.0 + 8.0, 12.0),
|
let color = if is_dark { "#ffffff" } else { "#000000" };
|
||||||
LabelType::Poi => (label.name.len() as f64 * 6.5 + 10.0, 14.0),
|
(font, color, label.name.len() as f64 * 16.0 + 20.0, 32.0)
|
||||||
LabelType::Transit => (label.name.len() as f64 * 10.0 + 16.0, 20.0), // Badge style
|
}
|
||||||
|
LabelType::City => {
|
||||||
|
let font = "600 18px Inter, -apple-system, sans-serif";
|
||||||
|
let color = if is_dark { "#ffffff" } else { "#000000" };
|
||||||
|
(font, color, label.name.len() as f64 * 12.0 + 10.0, 24.0)
|
||||||
|
}
|
||||||
|
LabelType::Street => {
|
||||||
|
let font = "500 14px Inter, -apple-system, sans-serif";
|
||||||
|
let color = if is_dark { "#cccccc" } else { "#333333" };
|
||||||
|
(font, color, label.name.len() as f64 * 8.0 + 8.0, 18.0)
|
||||||
|
}
|
||||||
|
LabelType::Poi => {
|
||||||
|
let font = "500 15px Inter, -apple-system, sans-serif";
|
||||||
|
let color = if is_dark { "#dddddd" } else { "#222222" };
|
||||||
|
(font, color, label.name.len() as f64 * 9.0 + 10.0, 20.0)
|
||||||
|
}
|
||||||
|
LabelType::Transit => {
|
||||||
|
let font = "bold 14px Inter, -apple-system, sans-serif";
|
||||||
|
let color = "#ffffff";
|
||||||
|
(font, color, label.name.len() as f64 * 12.0 + 16.0, 24.0)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let rect_x = css_x - est_w / 2.0;
|
let rect_x = css_x - est_w / 2.0;
|
||||||
@@ -376,35 +490,86 @@ pub fn update_labels(
|
|||||||
|
|
||||||
placed_rects.push((rect_x, rect_y, est_w, est_h));
|
placed_rects.push((rect_x, rect_y, est_w, est_h));
|
||||||
|
|
||||||
// Create DOM element
|
// Draw label on canvas
|
||||||
let div = document.create_element("div").unwrap();
|
ctx.set_font(font);
|
||||||
|
ctx.set_text_align("center");
|
||||||
|
ctx.set_text_baseline("middle");
|
||||||
|
|
||||||
let class_name = match label.label_type {
|
// Handle rotation for street labels
|
||||||
LabelType::Country => "label label-country".to_string(),
|
if label.label_type == LabelType::Street && label.rotation.abs() > 0.5 {
|
||||||
LabelType::City => "label label-city".to_string(),
|
ctx.save();
|
||||||
LabelType::Street => format!("label label-street label-street-{}", label.category),
|
let _ = ctx.translate(css_x, css_y);
|
||||||
LabelType::Poi => format!("label label-poi label-poi-{}", label.category),
|
let _ = ctx.rotate(label.rotation.to_radians());
|
||||||
LabelType::Transit => format!("label label-transit label-transit-{}", label.category),
|
|
||||||
|
// Draw text shadow for readability - stronger shadow
|
||||||
|
if is_dark {
|
||||||
|
ctx.set_shadow_blur(4.0);
|
||||||
|
ctx.set_shadow_color("rgba(0, 0, 0, 0.9)");
|
||||||
|
} else {
|
||||||
|
ctx.set_shadow_blur(3.0);
|
||||||
|
ctx.set_shadow_color("rgba(255, 255, 255, 1.0)");
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.set_fill_style(&color.into());
|
||||||
|
let _ = ctx.fill_text(&label.name, 0.0, 0.0);
|
||||||
|
ctx.restore();
|
||||||
|
} else if label.label_type == LabelType::Transit {
|
||||||
|
// Draw badge background for transit labels
|
||||||
|
let badge_width = est_w;
|
||||||
|
let badge_height = est_h;
|
||||||
|
|
||||||
|
let badge_color = match label.category.as_str() {
|
||||||
|
"sbahn" => "#408335", // S-Bahn green
|
||||||
|
"ubahn" => "#0065AE", // U-Bahn blue
|
||||||
|
_ => "#666666",
|
||||||
};
|
};
|
||||||
|
|
||||||
div.set_class_name(&class_name);
|
ctx.set_fill_style(&badge_color.into());
|
||||||
div.set_text_content(Some(&label.name));
|
|
||||||
|
|
||||||
let div_html: web_sys::HtmlElement = div.dyn_into().unwrap();
|
// Draw rounded rectangle for badge
|
||||||
let style = div_html.style();
|
if label.category == "sbahn" {
|
||||||
style.set_property("left", &format!("{}px", css_x)).unwrap();
|
// Circle for S-Bahn
|
||||||
style.set_property("top", &format!("{}px", css_y)).unwrap();
|
ctx.begin_path();
|
||||||
|
let _ = ctx.arc(css_x, css_y, 12.0, 0.0, std::f64::consts::PI * 2.0);
|
||||||
|
let _ = ctx.fill();
|
||||||
|
} else {
|
||||||
|
// Rounded rectangle for U-Bahn
|
||||||
|
let x = css_x - badge_width / 2.0;
|
||||||
|
let y = css_y - badge_height / 2.0;
|
||||||
|
let radius = 4.0;
|
||||||
|
|
||||||
let transform = match label.label_type {
|
ctx.begin_path();
|
||||||
LabelType::Poi => "translate(-50%, 10px)".to_string(),
|
let _ = ctx.move_to(x + radius, y);
|
||||||
LabelType::Street if label.rotation.abs() > 0.5 =>
|
let _ = ctx.line_to(x + badge_width - radius, y);
|
||||||
format!("translate(-50%, -50%) rotate({}deg)", label.rotation),
|
let _ = ctx.quadratic_curve_to(x + badge_width, y, x + badge_width, y + radius);
|
||||||
LabelType::Transit => "translate(-50%, -50%)".to_string(), // Centered badge
|
let _ = ctx.line_to(x + badge_width, y + badge_height - radius);
|
||||||
_ => "translate(-50%, -50%)".to_string(),
|
let _ = ctx.quadratic_curve_to(x + badge_width, y + badge_height, x + badge_width - radius, y + badge_height);
|
||||||
};
|
let _ = ctx.line_to(x + radius, y + badge_height);
|
||||||
|
let _ = ctx.quadratic_curve_to(x, y + badge_height, x, y + badge_height - radius);
|
||||||
|
let _ = ctx.line_to(x, y + radius);
|
||||||
|
let _ = ctx.quadratic_curve_to(x, y, x + radius, y);
|
||||||
|
let _ = ctx.close_path();
|
||||||
|
let _ = ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
style.set_property("transform", &transform).unwrap();
|
// Draw white text on badge
|
||||||
|
ctx.set_fill_style(&color.into());
|
||||||
|
let _ = ctx.fill_text(&label.name, css_x, css_y);
|
||||||
|
} else {
|
||||||
|
// Regular labels with stronger shadow
|
||||||
|
if is_dark {
|
||||||
|
ctx.set_shadow_blur(4.0);
|
||||||
|
ctx.set_shadow_color("rgba(0, 0, 0, 0.9)");
|
||||||
|
} else {
|
||||||
|
ctx.set_shadow_blur(3.0);
|
||||||
|
ctx.set_shadow_color("rgba(255, 255, 255, 1.0)");
|
||||||
|
}
|
||||||
|
|
||||||
container.append_child(&div_html).unwrap();
|
ctx.set_fill_style(&color.into());
|
||||||
|
let _ = ctx.fill_text(&label.name, css_x, css_y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset shadow
|
||||||
|
ctx.set_shadow_blur(0.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -491,6 +491,7 @@ pub async fn run() {
|
|||||||
rpass.set_bind_group(0, &camera_bind_group, &[]);
|
rpass.set_bind_group(0, &camera_bind_group, &[]);
|
||||||
for buffers in &tiles_to_render {
|
for buffers in &tiles_to_render {
|
||||||
if buffers.water_index_count > 0 {
|
if buffers.water_index_count > 0 {
|
||||||
|
rpass.set_bind_group(1, &buffers.tile_bind_group, &[]);
|
||||||
rpass.set_vertex_buffer(0, buffers.water_vertex_buffer.slice(..));
|
rpass.set_vertex_buffer(0, buffers.water_vertex_buffer.slice(..));
|
||||||
rpass.draw(0..buffers.water_index_count, 0..1);
|
rpass.draw(0..buffers.water_index_count, 0..1);
|
||||||
}
|
}
|
||||||
@@ -588,9 +589,8 @@ pub async fn run() {
|
|||||||
aspect: camera_guard.aspect
|
aspect: camera_guard.aspect
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update labels every frame to ensure they move smoothly with the map
|
// Update labels every frame for smooth dragging
|
||||||
// Performance is maintained through zoom-based label limits in labels.rs
|
// Performance is maintained through reduced label limits (20/100/300/500)
|
||||||
last_label_camera = (camera_guard.x, camera_guard.y, camera_guard.zoom);
|
|
||||||
update_labels(
|
update_labels(
|
||||||
&web_sys::window().unwrap(),
|
&web_sys::window().unwrap(),
|
||||||
&temp_camera,
|
&temp_camera,
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ use super::common::{Vertex, create_simple_pipeline};
|
|||||||
pub fn create_water_pipeline(
|
pub fn create_water_pipeline(
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
format: &wgpu::TextureFormat,
|
format: &wgpu::TextureFormat,
|
||||||
bind_group_layout: &wgpu::BindGroupLayout
|
camera_bind_group_layout: &wgpu::BindGroupLayout,
|
||||||
|
tile_bind_group_layout: &wgpu::BindGroupLayout,
|
||||||
) -> wgpu::RenderPipeline {
|
) -> wgpu::RenderPipeline {
|
||||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
label: None,
|
label: None,
|
||||||
@@ -17,8 +18,16 @@ pub fn create_water_pipeline(
|
|||||||
@group(0) @binding(0)
|
@group(0) @binding(0)
|
||||||
var<uniform> camera: CameraUniform;
|
var<uniform> camera: CameraUniform;
|
||||||
|
|
||||||
|
struct TileUniform {
|
||||||
|
origin: vec2<f32>, // Tile origin in global 0..1 space
|
||||||
|
scale: f32, // Tile size in global space (1.0 / 2^zoom)
|
||||||
|
_padding: f32,
|
||||||
|
};
|
||||||
|
@group(1) @binding(0)
|
||||||
|
var<uniform> tile: TileUniform;
|
||||||
|
|
||||||
struct VertexInput {
|
struct VertexInput {
|
||||||
@location(0) position: vec2<f32>,
|
@location(0) position: vec2<f32>, // 0..1 relative to tile
|
||||||
};
|
};
|
||||||
|
|
||||||
struct VertexOutput {
|
struct VertexOutput {
|
||||||
@@ -31,15 +40,12 @@ pub fn create_water_pipeline(
|
|||||||
) -> VertexOutput {
|
) -> VertexOutput {
|
||||||
var out: VertexOutput;
|
var out: VertexOutput;
|
||||||
|
|
||||||
let world_pos = model.position;
|
// Transform from tile-relative (0..1) to global (0..1)
|
||||||
|
let world_pos = tile.origin + model.position * tile.scale;
|
||||||
|
|
||||||
let x = world_pos.x * camera.params.x + camera.params.z;
|
let x = world_pos.x * camera.params.x + camera.params.z;
|
||||||
let y = world_pos.y * camera.params.y + camera.params.w;
|
let y = world_pos.y * camera.params.y + camera.params.w;
|
||||||
|
|
||||||
// Globe Effect: Spherize
|
|
||||||
// let r2 = x*x + y*y;
|
|
||||||
// let w = 1.0 + r2 * 0.5;
|
|
||||||
|
|
||||||
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
|
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -62,7 +68,7 @@ pub fn create_water_pipeline(
|
|||||||
|
|
||||||
let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
label: Some("Water Pipeline Layout"),
|
label: Some("Water Pipeline Layout"),
|
||||||
bind_group_layouts: &[bind_group_layout],
|
bind_group_layouts: &[camera_bind_group_layout, tile_bind_group_layout],
|
||||||
push_constant_ranges: &[],
|
push_constant_ranges: &[],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -42,13 +42,33 @@ pub struct RenderService {
|
|||||||
pub landuse_green_pipeline: wgpu::RenderPipeline,
|
pub landuse_green_pipeline: wgpu::RenderPipeline,
|
||||||
pub landuse_residential_pipeline: wgpu::RenderPipeline,
|
pub landuse_residential_pipeline: wgpu::RenderPipeline,
|
||||||
pub sand_pipeline: wgpu::RenderPipeline,
|
pub sand_pipeline: wgpu::RenderPipeline,
|
||||||
|
|
||||||
|
// Layouts
|
||||||
|
pub tile_bind_group_layout: wgpu::BindGroupLayout,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderService {
|
impl RenderService {
|
||||||
pub fn new(device: &wgpu::Device, format: &wgpu::TextureFormat, camera_layout: &wgpu::BindGroupLayout) -> Self {
|
pub fn new(device: &wgpu::Device, format: &wgpu::TextureFormat, camera_layout: &wgpu::BindGroupLayout) -> Self {
|
||||||
|
// Create tile bind group layout for tile-relative coordinates
|
||||||
|
let tile_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 0,
|
||||||
|
visibility: wgpu::ShaderStages::VERTEX,
|
||||||
|
ty: wgpu::BindingType::Buffer {
|
||||||
|
ty: wgpu::BufferBindingType::Uniform,
|
||||||
|
has_dynamic_offset: false,
|
||||||
|
min_binding_size: None,
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
label: Some("tile_bind_group_layout"),
|
||||||
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
building_pipeline: create_colored_building_pipeline(device, format, camera_layout),
|
building_pipeline: create_colored_building_pipeline(device, format, camera_layout),
|
||||||
water_pipeline: create_water_pipeline(device, format, camera_layout),
|
water_pipeline: create_water_pipeline(device, format, camera_layout, &tile_bind_group_layout),
|
||||||
water_line_pipeline: create_water_line_pipeline(device, format, camera_layout),
|
water_line_pipeline: create_water_line_pipeline(device, format, camera_layout),
|
||||||
railway_pipeline: create_railway_pipeline(device, format, camera_layout),
|
railway_pipeline: create_railway_pipeline(device, format, camera_layout),
|
||||||
motorway_outline: create_road_motorway_outline_pipeline(device, format, camera_layout),
|
motorway_outline: create_road_motorway_outline_pipeline(device, format, camera_layout),
|
||||||
@@ -62,6 +82,7 @@ impl RenderService {
|
|||||||
landuse_green_pipeline: create_landuse_green_pipeline(device, format, camera_layout),
|
landuse_green_pipeline: create_landuse_green_pipeline(device, format, camera_layout),
|
||||||
landuse_residential_pipeline: create_landuse_residential_pipeline(device, format, camera_layout),
|
landuse_residential_pipeline: create_landuse_residential_pipeline(device, format, camera_layout),
|
||||||
sand_pipeline: create_sand_pipeline(device, format, camera_layout),
|
sand_pipeline: create_sand_pipeline(device, format, camera_layout),
|
||||||
|
tile_bind_group_layout,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,73 +244,26 @@ impl RenderService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process water
|
// Process water using pre-computed vertex buffers (tile-relative coordinates)
|
||||||
if let Some(water) = state.water.get(&tile) {
|
if let Some(water) = state.water.get(&tile) {
|
||||||
for w in water {
|
for w in water {
|
||||||
// Check if it's a waterway (line) or water body (polygon)
|
// Use pre-computed vertex buffer from importer
|
||||||
let is_line = w.tags.contains_key("waterway");
|
// The buffer contains tile-relative coordinates (0..1 within tile)
|
||||||
|
if w.vertex_buffer.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let mut poly_points: Vec<(f64, f64)> = Vec::new();
|
// Vertex buffer format: [f32; 2] positions in tile-relative space
|
||||||
for chunk in w.points.chunks(8) {
|
for chunk in w.vertex_buffer.chunks(8) {
|
||||||
if chunk.len() < 8 { break; }
|
if chunk.len() < 8 { break; }
|
||||||
let lat = f32::from_le_bytes(chunk[0..4].try_into().unwrap_or([0u8; 4]));
|
let x = f32::from_le_bytes(chunk[0..4].try_into().unwrap_or([0u8; 4]));
|
||||||
let lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap_or([0u8; 4]));
|
let y = f32::from_le_bytes(chunk[4..8].try_into().unwrap_or([0u8; 4]));
|
||||||
poly_points.push((lat as f64, lon as f64));
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_line {
|
|
||||||
// Simple line strip, expand to thin quad strip?
|
|
||||||
// Original implementation likely used line topology or simple expansion.
|
|
||||||
// Let's assume line strip for now or thin quad.
|
|
||||||
// Wait, previous water line pipeline probably handles lines?
|
|
||||||
// But pipelines::roads::generate_road_geometry makes quads.
|
|
||||||
// For now, let's treat as Triangles for consistency via triangulation?
|
|
||||||
// No, lines cannot be triangulated by earcut.
|
|
||||||
// I'll project and push as line vertices?
|
|
||||||
// But topology is TriangleList for all?
|
|
||||||
// render_service uses `water_line_pipeline`.
|
|
||||||
// If pipeline is line list, then points are enough.
|
|
||||||
// Let's check pipelines/water.rs? Unsure.
|
|
||||||
// Assuming line list for now or reusing road geometry with fixed width.
|
|
||||||
// The previous code had `create_water_line_pipeline`.
|
|
||||||
// Let's project and create simple line segments?
|
|
||||||
// Or reuse triangulate? No.
|
|
||||||
// Let's generate a "thin road" using road geometry for consistency?
|
|
||||||
let centers: Vec<[f32; 2]> = poly_points.iter().map(|&(lat, lon)| {
|
|
||||||
let (x, y) = project(lat, lon);
|
|
||||||
[x as f32, y as f32]
|
|
||||||
}).collect();
|
|
||||||
// Use road geometry for lines with fixed width
|
|
||||||
let _geom = pipelines::roads::generate_road_geometry(¢ers, 3.0, 3.0); // 3m width
|
|
||||||
// Cast RoadVertex to Vertex (drop normal/lanes)?
|
|
||||||
// No, target is `Vec<Vertex>`.
|
|
||||||
// RoadVertex has normal/lanes. Vertex only position.
|
|
||||||
// I'll manually generate a quad strip using normals logic inline?
|
|
||||||
// Or just use points?
|
|
||||||
// Actually, if I use `generate_road_geometry` I get `RoadVertex`.
|
|
||||||
// Let's assume water lines are just lines.
|
|
||||||
// I will push points as lines if topology supports it.
|
|
||||||
// But `create_buffer_init` usage is generic.
|
|
||||||
// Let's use `generate_road_geometry` logic but simplify to Vertex.
|
|
||||||
// Or better: Just generate a thin strip.
|
|
||||||
// I'll use simple earcut (if polygon) or just thick line logic.
|
|
||||||
// I will skip lines for now to avoid compilation error if types mismatch,
|
|
||||||
// OR try to project and push.
|
|
||||||
// Wait, `Vertex` has just position.
|
|
||||||
// I'll assume TriangleList topology.
|
|
||||||
// I'll use earcut for water polygons.
|
|
||||||
} else {
|
|
||||||
// Polygon
|
|
||||||
let triangulated = GeometryService::triangulate_polygon(&poly_points);
|
|
||||||
for (lat, lon) in triangulated {
|
|
||||||
let (x, y) = project(lat, lon);
|
|
||||||
water_vertex_data.push(Vertex {
|
water_vertex_data.push(Vertex {
|
||||||
position: [x as f32, y as f32],
|
position: [x, y], // Already in tile-relative coordinates
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Process railways
|
// Process railways
|
||||||
if let Some(railways) = state.railways.get(&tile) {
|
if let Some(railways) = state.railways.get(&tile) {
|
||||||
@@ -387,6 +361,64 @@ impl RenderService {
|
|||||||
|
|
||||||
// Only insert buffers if we actually created something
|
// Only insert buffers if we actually created something
|
||||||
if created_buffers {
|
if created_buffers {
|
||||||
|
// Calculate tile origin and scale for tile-relative coordinates
|
||||||
|
let (z, x, y) = tile;
|
||||||
|
let tile_count = 2_f32.powi(z);
|
||||||
|
let tile_size = 1.0 / tile_count;
|
||||||
|
let tile_origin_x = x as f32 * tile_size;
|
||||||
|
let tile_origin_y = y as f32 * tile_size;
|
||||||
|
|
||||||
|
// Create tile uniform buffer
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
|
struct TileUniform {
|
||||||
|
origin: [f32; 2],
|
||||||
|
scale: f32,
|
||||||
|
_padding: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
let tile_uniform = TileUniform {
|
||||||
|
origin: [tile_origin_x, tile_origin_y],
|
||||||
|
scale: tile_size,
|
||||||
|
_padding: 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let tile_uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||||
|
label: Some("Tile Uniform Buffer"),
|
||||||
|
contents: bytemuck::cast_slice(&[tile_uniform]),
|
||||||
|
usage: wgpu::BufferUsages::UNIFORM,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get RenderService to access tile_bind_group_layout
|
||||||
|
// We need to pass it from lib.rs or store it somewhere accessible
|
||||||
|
// For now, we'll recreate it (not ideal but functional)
|
||||||
|
let tile_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 0,
|
||||||
|
visibility: wgpu::ShaderStages::VERTEX,
|
||||||
|
ty: wgpu::BindingType::Buffer {
|
||||||
|
ty: wgpu::BufferBindingType::Uniform,
|
||||||
|
has_dynamic_offset: false,
|
||||||
|
min_binding_size: None,
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
label: Some("tile_bind_group_layout"),
|
||||||
|
});
|
||||||
|
|
||||||
|
let tile_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
layout: &tile_bind_group_layout,
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: tile_uniform_buffer.as_entire_binding(),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
label: Some("Tile Bind Group"),
|
||||||
|
});
|
||||||
|
|
||||||
state.buffers.insert(tile, std::sync::Arc::new(TileBuffers {
|
state.buffers.insert(tile, std::sync::Arc::new(TileBuffers {
|
||||||
road_motorway_vertex_buffer,
|
road_motorway_vertex_buffer,
|
||||||
road_motorway_vertex_count,
|
road_motorway_vertex_count,
|
||||||
@@ -410,6 +442,7 @@ impl RenderService {
|
|||||||
railway_vertex_count,
|
railway_vertex_count,
|
||||||
water_line_vertex_buffer,
|
water_line_vertex_buffer,
|
||||||
water_line_vertex_count,
|
water_line_vertex_count,
|
||||||
|
tile_bind_group,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ pub struct TileBuffers {
|
|||||||
|
|
||||||
pub water_line_vertex_buffer: wgpu::Buffer,
|
pub water_line_vertex_buffer: wgpu::Buffer,
|
||||||
pub water_line_vertex_count: u32,
|
pub water_line_vertex_count: u32,
|
||||||
|
|
||||||
|
// Tile-specific uniform bind group for relative coordinates
|
||||||
|
pub tile_bind_group: wgpu::BindGroup,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Type of label for styling
|
/// Type of label for styling
|
||||||
|
|||||||
@@ -269,13 +269,29 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
if is_highway || treat_as_water_line {
|
if is_highway || treat_as_water_line {
|
||||||
// Generate road geometry
|
// Generate road geometry
|
||||||
let projected_points: Vec<[f32; 2]> = simplified_points.iter()
|
let projected_points_raw: Vec<[f32; 2]> = simplified_points.iter()
|
||||||
.map(|(lat, lon)| {
|
.map(|(lat, lon)| {
|
||||||
let (x, y) = GeometryService::project(*lat, *lon);
|
let (x, y) = GeometryService::project(*lat, *lon);
|
||||||
[x, y]
|
[x, y]
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
// Fix degenerate segments: Deduplicate consecutive points that are too close
|
||||||
|
let mut projected_points = Vec::with_capacity(projected_points_raw.len());
|
||||||
|
if !projected_points_raw.is_empty() {
|
||||||
|
projected_points.push(projected_points_raw[0]);
|
||||||
|
for i in 1..projected_points_raw.len() {
|
||||||
|
let prev = projected_points.last().unwrap();
|
||||||
|
let curr = projected_points_raw[i];
|
||||||
|
let dx = curr[0] - prev[0];
|
||||||
|
let dy = curr[1] - prev[1];
|
||||||
|
// 1.0e-11 is approx (3e-6)^2, which ensures we are safely above the 1e-6 degenerate threshold
|
||||||
|
if (dx * dx + dy * dy) > 1.0e-11 {
|
||||||
|
projected_points.push(curr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let highway_tag = tags.get("highway").map(|s| s.as_str());
|
let highway_tag = tags.get("highway").map(|s| s.as_str());
|
||||||
let road_type = match highway_tag.unwrap_or("") {
|
let road_type = match highway_tag.unwrap_or("") {
|
||||||
"motorway" | "motorway_link" | "trunk" | "trunk_link" => 0.0,
|
"motorway" | "motorway_link" | "trunk" | "trunk_link" => 0.0,
|
||||||
@@ -293,20 +309,67 @@ async fn main() -> Result<()> {
|
|||||||
.and_then(|s| s.parse().ok())
|
.and_then(|s| s.parse().ok())
|
||||||
.unwrap_or(default_lanes);
|
.unwrap_or(default_lanes);
|
||||||
|
|
||||||
// DEBUG: Log first way to validate mesh generation
|
// DEBUG: Enable verbose logging for specific way ID via environment variable
|
||||||
if way_count == 1 {
|
let debug_way_id: Option<i64> = std::env::var("DEBUG_WAY_ID")
|
||||||
println!("DEBUG Way {}: {} projected points, generating mesh...", id, projected_points.len());
|
.ok()
|
||||||
|
.and_then(|s| s.parse().ok());
|
||||||
|
|
||||||
|
if debug_way_id == Some(id) {
|
||||||
|
println!("DEBUG Way {}: Processing at zoom {}", id, zoom);
|
||||||
|
println!(" - is_highway: {}", is_highway);
|
||||||
|
println!(" - treat_as_water_line: {}", treat_as_water_line);
|
||||||
|
println!(" - is_water_line: {}", is_water_line);
|
||||||
|
println!(" - is_water_area: {}", is_water_area);
|
||||||
|
println!(" - Original points: {}", points.len());
|
||||||
|
println!(" - Simplified points: {}", simplified_points.len());
|
||||||
|
println!(" - Projected points: {}", projected_points.len());
|
||||||
|
println!(" - First 5 simplified (lat/lon):");
|
||||||
|
for (i, p) in simplified_points.iter().take(5).enumerate() {
|
||||||
|
println!(" [{:2}] lat={:.8}, lon={:.8}", i, p.0, p.1);
|
||||||
|
}
|
||||||
|
println!(" - First 5 projected (x/y):");
|
||||||
|
for (i, p) in projected_points.iter().take(5).enumerate() {
|
||||||
|
println!(" [{:2}] x={:.8}, y={:.8}", i, p[0], p[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for consecutive duplicates
|
||||||
|
let mut duplicates = 0;
|
||||||
|
for i in 0..projected_points.len().saturating_sub(1) {
|
||||||
|
let p1 = projected_points[i];
|
||||||
|
let p2 = projected_points[i + 1];
|
||||||
|
let dx = p2[0] - p1[0];
|
||||||
|
let dy = p2[1] - p1[1];
|
||||||
|
let dist = (dx * dx + dy * dy).sqrt();
|
||||||
|
if dist < 0.000001 {
|
||||||
|
duplicates += 1;
|
||||||
|
if duplicates <= 3 {
|
||||||
|
println!(" DEGENERATE segment {}: dist={:.12}, p1=[{:.8},{:.8}], p2=[{:.8},{:.8}]",
|
||||||
|
i, dist, p1[0], p1[1], p2[0], p2[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if duplicates > 0 {
|
||||||
|
println!(" - Total degenerate segments: {}/{}", duplicates, projected_points.len() - 1);
|
||||||
|
}
|
||||||
|
println!(" - Tags: {:?}", tags);
|
||||||
}
|
}
|
||||||
|
|
||||||
let vertex_buffer = if treat_as_water_line {
|
let vertex_buffer = if treat_as_water_line {
|
||||||
|
if debug_way_id == Some(id) {
|
||||||
|
println!(" - Using generate_polygon_geometry for water line");
|
||||||
|
}
|
||||||
mesh_svc.generate_polygon_geometry(&projected_points)
|
mesh_svc.generate_polygon_geometry(&projected_points)
|
||||||
} else {
|
} else {
|
||||||
|
if debug_way_id == Some(id) {
|
||||||
|
println!(" - Using generate_road_geometry with lanes={}, road_type={}", lanes, road_type);
|
||||||
|
}
|
||||||
mesh_svc.generate_road_geometry(&projected_points, lanes, road_type)
|
mesh_svc.generate_road_geometry(&projected_points, lanes, road_type)
|
||||||
};
|
};
|
||||||
|
|
||||||
// DEBUG: Log buffer size
|
// DEBUG: Log buffer size for tracked way
|
||||||
if way_count == 1 {
|
if debug_way_id == Some(id) {
|
||||||
println!("DEBUG Way {}: vertex_buffer size = {} bytes", id, vertex_buffer.len());
|
println!(" - vertex_buffer size = {} bytes", vertex_buffer.len());
|
||||||
|
println!(" - Expected vertex size: {} bytes", if treat_as_water_line { 8 } else { 24 });
|
||||||
}
|
}
|
||||||
|
|
||||||
let task = DbTask::Way {
|
let task = DbTask::Way {
|
||||||
@@ -358,11 +421,40 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if treat_as_water_area {
|
if treat_as_water_area {
|
||||||
// Generate water polygon mesh
|
// Calculate bounding box for multi-tile insertion
|
||||||
|
let mut min_lat = f64::MAX;
|
||||||
|
let mut max_lat = f64::MIN;
|
||||||
|
let mut min_lon = f64::MAX;
|
||||||
|
let mut max_lon = f64::MIN;
|
||||||
|
|
||||||
|
for (lat, lon) in &final_points {
|
||||||
|
if *lat < min_lat { min_lat = *lat; }
|
||||||
|
if *lat > max_lat { max_lat = *lat; }
|
||||||
|
if *lon < min_lon { min_lon = *lon; }
|
||||||
|
if *lon > max_lon { max_lon = *lon; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get tiles covered by bounding box
|
||||||
|
let (min_tile_x, min_tile_y) = TileService::lat_lon_to_tile(min_lat, min_lon, zoom);
|
||||||
|
let (max_tile_x, max_tile_y) = TileService::lat_lon_to_tile(max_lat, max_lon, zoom);
|
||||||
|
|
||||||
|
// Iterate over all tiles in bbox
|
||||||
|
for tile_x in min_tile_x..=max_tile_x {
|
||||||
|
for tile_y in min_tile_y..=max_tile_y {
|
||||||
|
// Calculate tile origin and scale for relative coordinates
|
||||||
|
let tile_count = 2_f64.powi(zoom as i32);
|
||||||
|
let tile_size = 1.0 / tile_count;
|
||||||
|
let tile_origin_x = tile_x as f64 * tile_size;
|
||||||
|
let tile_origin_y = tile_y as f64 * tile_size;
|
||||||
|
|
||||||
|
// Project points to global space then make relative to this tile
|
||||||
let projected_points: Vec<[f32; 2]> = final_points.iter()
|
let projected_points: Vec<[f32; 2]> = final_points.iter()
|
||||||
.map(|(lat, lon)| {
|
.map(|(lat, lon)| {
|
||||||
let (x, y) = GeometryService::project(*lat, *lon);
|
let (global_x, global_y) = GeometryService::project(*lat, *lon);
|
||||||
[x, y]
|
// Convert to tile-relative (0..1 within tile)
|
||||||
|
let relative_x = ((global_x as f64 - tile_origin_x) / tile_size) as f32;
|
||||||
|
let relative_y = ((global_y as f64 - tile_origin_y) / tile_size) as f32;
|
||||||
|
[relative_x, relative_y]
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -375,11 +467,13 @@ async fn main() -> Result<()> {
|
|||||||
tags: tags.clone(),
|
tags: tags.clone(),
|
||||||
points: polygon_blob.clone(),
|
points: polygon_blob.clone(),
|
||||||
vertex_buffer,
|
vertex_buffer,
|
||||||
x,
|
x: tile_x,
|
||||||
y
|
y: tile_y,
|
||||||
};
|
};
|
||||||
let _ = tx.blocking_send(task);
|
let _ = tx.blocking_send(task);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if treat_as_landuse {
|
if treat_as_landuse {
|
||||||
// Generate landuse polygon mesh
|
// Generate landuse polygon mesh
|
||||||
@@ -434,8 +528,11 @@ async fn main() -> Result<()> {
|
|||||||
if let Some(line_ref) = tags.get("ref") {
|
if let Some(line_ref) = tags.get("ref") {
|
||||||
// Only propagate S-Bahn/U-Bahn style refs (starts with S or U followed by digit)
|
// 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 {
|
if (line_ref.starts_with('S') || line_ref.starts_with('U')) && line_ref.len() >= 2 {
|
||||||
|
// Only log if verbose debugging is enabled
|
||||||
|
if std::env::var("VERBOSE_DEBUG").is_ok() {
|
||||||
let member_count = rel.members().filter(|m| matches!(m.member_type, osmpbf::RelMemberType::Way)).count();
|
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);
|
println!("DEBUG: Found transit line ref '{}' with {} way members", line_ref, member_count);
|
||||||
|
}
|
||||||
for member in rel.members() {
|
for member in rel.members() {
|
||||||
if let osmpbf::RelMemberType::Way = member.member_type {
|
if let osmpbf::RelMemberType::Way = member.member_type {
|
||||||
railway_store.set_ref(member.member_id, line_ref.clone());
|
railway_store.set_ref(member.member_id, line_ref.clone());
|
||||||
@@ -497,7 +594,60 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
let table = if is_water { "water" } else { "landuse" };
|
let table = if is_water { "water" } else { "landuse" };
|
||||||
|
|
||||||
// Generate polygon mesh for multipolygons
|
// For water, use multi-tile insertion with tile-relative coords
|
||||||
|
if is_water {
|
||||||
|
// Calculate bounding box
|
||||||
|
let mut min_lat = f64::MAX;
|
||||||
|
let mut max_lat = f64::MIN;
|
||||||
|
let mut min_lon = f64::MAX;
|
||||||
|
let mut max_lon = f64::MIN;
|
||||||
|
|
||||||
|
for (lat, lon) in &final_points {
|
||||||
|
if *lat < min_lat { min_lat = *lat; }
|
||||||
|
if *lat > max_lat { max_lat = *lat; }
|
||||||
|
if *lon < min_lon { min_lon = *lon; }
|
||||||
|
if *lon > max_lon { max_lon = *lon; }
|
||||||
|
}
|
||||||
|
|
||||||
|
let (min_tile_x, min_tile_y) = TileService::lat_lon_to_tile(min_lat, min_lon, zoom);
|
||||||
|
let (max_tile_x, max_tile_y) = TileService::lat_lon_to_tile(max_lat, max_lon, zoom);
|
||||||
|
|
||||||
|
// Iterate over all tiles
|
||||||
|
for tile_x in min_tile_x..=max_tile_x {
|
||||||
|
for tile_y in min_tile_y..=max_tile_y {
|
||||||
|
let tile_count = 2_f64.powi(zoom as i32);
|
||||||
|
let tile_size = 1.0 / tile_count;
|
||||||
|
let tile_origin_x = tile_x as f64 * tile_size;
|
||||||
|
let tile_origin_y = tile_y as f64 * tile_size;
|
||||||
|
|
||||||
|
let projected_points: Vec<[f32; 2]> = final_points.iter()
|
||||||
|
.map(|(lat, lon)| {
|
||||||
|
let (global_x, global_y) = GeometryService::project(*lat, *lon);
|
||||||
|
let relative_x = ((global_x as f64 - tile_origin_x) / tile_size) as f32;
|
||||||
|
let relative_y = ((global_y as f64 - tile_origin_y) / tile_size) as f32;
|
||||||
|
[relative_x, relative_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: tile_x,
|
||||||
|
y: tile_y,
|
||||||
|
};
|
||||||
|
let _ = tx.blocking_send(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Landuse: keep old single-tile logic for now
|
||||||
|
let (x, y) = TileService::lat_lon_to_tile(first_lat, first_lon, zoom);
|
||||||
|
|
||||||
let projected_points: Vec<[f32; 2]> = final_points.iter()
|
let projected_points: Vec<[f32; 2]> = final_points.iter()
|
||||||
.map(|(lat, lon)| {
|
.map(|(lat, lon)| {
|
||||||
let (x, y) = GeometryService::project(*lat, *lon);
|
let (x, y) = GeometryService::project(*lat, *lon);
|
||||||
@@ -515,7 +665,7 @@ async fn main() -> Result<()> {
|
|||||||
points: polygon_blob.clone(),
|
points: polygon_blob.clone(),
|
||||||
vertex_buffer,
|
vertex_buffer,
|
||||||
x,
|
x,
|
||||||
y
|
y,
|
||||||
};
|
};
|
||||||
let _ = tx.blocking_send(task);
|
let _ = tx.blocking_send(task);
|
||||||
}
|
}
|
||||||
@@ -525,6 +675,7 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -158,12 +158,18 @@ impl MeshGenerationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn generate_road_mesh(points: &[[f32; 2]], lanes: f32, road_type: f32) -> Vec<RoadVertex> {
|
fn generate_road_mesh(points: &[[f32; 2]], lanes: f32, road_type: f32) -> Vec<RoadVertex> {
|
||||||
|
let debug_mesh = std::env::var("DEBUG_MESH").is_ok();
|
||||||
|
|
||||||
if points.len() < 2 {
|
if points.len() < 2 {
|
||||||
|
if debug_mesh {
|
||||||
|
println!("DEBUG MESH: Too few points ({})", points.len());
|
||||||
|
}
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute normals for each segment
|
// Compute normals for each segment
|
||||||
let mut segment_normals = Vec::with_capacity(points.len() - 1);
|
let mut segment_normals = Vec::with_capacity(points.len() - 1);
|
||||||
|
let mut degenerate_count = 0;
|
||||||
for i in 0..points.len() - 1 {
|
for i in 0..points.len() - 1 {
|
||||||
let p1 = points[i];
|
let p1 = points[i];
|
||||||
let p2 = points[i + 1];
|
let p2 = points[i + 1];
|
||||||
@@ -172,11 +178,16 @@ impl MeshGenerationService {
|
|||||||
let len = (dx * dx + dy * dy).sqrt();
|
let len = (dx * dx + dy * dy).sqrt();
|
||||||
if len < 0.000001 {
|
if len < 0.000001 {
|
||||||
segment_normals.push([0.0, 0.0]);
|
segment_normals.push([0.0, 0.0]);
|
||||||
|
degenerate_count += 1;
|
||||||
} else {
|
} else {
|
||||||
segment_normals.push([-dy / len, dx / len]);
|
segment_normals.push([-dy / len, dx / len]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if debug_mesh && degenerate_count > 0 {
|
||||||
|
println!("DEBUG MESH: {}/{} segments degenerate", degenerate_count, segment_normals.len());
|
||||||
|
}
|
||||||
|
|
||||||
// Generate vertex pairs with miter joins
|
// Generate vertex pairs with miter joins
|
||||||
let mut point_pairs = Vec::with_capacity(points.len() * 2);
|
let mut point_pairs = Vec::with_capacity(points.len() * 2);
|
||||||
|
|
||||||
@@ -233,8 +244,10 @@ impl MeshGenerationService {
|
|||||||
|
|
||||||
// Triangulate
|
// Triangulate
|
||||||
let mut triangle_vertices = Vec::with_capacity((points.len() - 1) * 6);
|
let mut triangle_vertices = Vec::with_capacity((points.len() - 1) * 6);
|
||||||
|
let mut skipped = 0;
|
||||||
for i in 0..points.len() - 1 {
|
for i in 0..points.len() - 1 {
|
||||||
if Self::dot(segment_normals[i], segment_normals[i]) == 0.0 {
|
if Self::dot(segment_normals[i], segment_normals[i]) == 0.0 {
|
||||||
|
skipped += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,6 +268,11 @@ impl MeshGenerationService {
|
|||||||
triangle_vertices.push(v3);
|
triangle_vertices.push(v3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if debug_mesh {
|
||||||
|
println!("DEBUG MESH: Generated {} vertices from {} points (skipped {} segments)",
|
||||||
|
triangle_vertices.len(), points.len(), skipped);
|
||||||
|
}
|
||||||
|
|
||||||
triangle_vertices
|
triangle_vertices
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user