This commit is contained in:
Dongho Kim
2025-12-17 16:42:23 +09:00
parent 8edb92b25d
commit 4b606e28da
12 changed files with 991 additions and 195 deletions

View File

@@ -4,7 +4,8 @@ WORKDIR /app
COPY frontend ./frontend COPY frontend ./frontend
COPY backend ./backend COPY backend ./backend
# Install wasm-pack # Install wasm-pack
RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh # Install wasm-pack via cargo (fallback for network issues)
RUN cargo install wasm-pack
WORKDIR /app/frontend WORKDIR /app/frontend
# Build frontend # Build frontend
RUN wasm-pack build --target web --out-name wasm --out-dir ../backend/static RUN wasm-pack build --target web --out-name wasm --out-dir ../backend/static

View File

@@ -573,6 +573,51 @@
0 0 4px rgba(255, 255, 255, 0.9); 0 0 4px rgba(255, 255, 255, 0.9);
} }
/* Road type based sizing - larger roads get larger fonts */
.label-street-motorway {
font-size: 13px;
font-weight: 600;
color: #444;
}
.label-street-primary {
font-size: 12px;
font-weight: 600;
color: #555;
}
.label-street-secondary {
font-size: 11px;
font-weight: 500;
color: #5e5e63;
}
.label-street-tertiary {
font-size: 10px;
font-weight: 500;
}
.label-street-residential {
font-size: 9px;
font-weight: 400;
color: #7e7e83;
}
/* One-way direction arrows */
.oneway-arrow {
position: absolute;
font-size: 18px;
font-weight: bold;
color: rgba(100, 100, 100, 0.5);
pointer-events: none;
text-shadow: none;
z-index: 5;
}
[data-theme="dark"] .oneway-arrow {
color: rgba(180, 180, 180, 0.5);
}
/* Apple Maps-style POI labels */ /* Apple Maps-style POI labels */
.label-poi { .label-poi {
font-size: 11px; font-size: 11px;
@@ -1279,7 +1324,7 @@
</style> </style>
<script type="module"> <script type="module">
import init from './wasm.js?v=fixed_labels_v6'; import init from './wasm.js?v=fixed_labels_v20';
async function run() { async function run() {
try { try {

View File

@@ -199,29 +199,32 @@ pub fn update_labels(
} }
} }
// Process ways for street labels // Process ways for street labels - deduplicate by name
let client_width = window.inner_width().ok().and_then(|v| v.as_f64()).unwrap_or(width); let client_width = window.inner_width().ok().and_then(|v| v.as_f64()).unwrap_or(width);
let client_height = window.inner_height().ok().and_then(|v| v.as_f64()).unwrap_or(height); let client_height = window.inner_height().ok().and_then(|v| v.as_f64()).unwrap_or(height);
// Collect all street ways and group by name to deduplicate
use std::collections::HashMap;
let mut street_ways: HashMap<String, Vec<(&crate::types::MapWay, i32)>> = HashMap::new();
for tile in &visible_tiles { for tile in &visible_tiles {
if let Some(ways) = state.ways.get(tile) { if let Some(ways) = state.ways.get(tile) {
for way in ways { for way in ways {
// Check if road has a name
let name: Option<&str> = way.tags.get("name").map(|s| s.as_str()); let name: Option<&str> = way.tags.get("name").map(|s| s.as_str());
let highway: Option<&str> = way.tags.get("highway").map(|s| s.as_str()); let highway: Option<&str> = way.tags.get("highway").map(|s| s.as_str());
if let (Some(name), Some(highway_type)) = (name, highway) { if let (Some(name), Some(highway_type)) = (name, highway) {
// Skip unnamed or minor roads
if name.is_empty() { continue; } if name.is_empty() { continue; }
// Zoom filtering // Zoom filtering - only show labels when roads are clearly visible
// Higher number = requires more zoom (more zoomed in)
let min_zoom = match highway_type { let min_zoom = match highway_type {
"motorway" | "trunk" => 200.0, "motorway" | "trunk" => 5000.0, // Show early (major highways)
"primary" => 500.0, "primary" => 20000.0, // Medium zoom
"secondary" => 1500.0, "secondary" => 50000.0, // Need more zoom
"tertiary" => 3000.0, "tertiary" => 100000.0, // Even more zoom
"residential" | "unclassified" => 6000.0, "residential" | "unclassified" => 300000.0, // Only when very zoomed in
_ => 10000.0, _ => 500000.0, // Minor roads - extremely zoomed in
}; };
if zoom < min_zoom { continue; } if zoom < min_zoom { continue; }
@@ -234,64 +237,106 @@ pub fn update_labels(
_ => 10, _ => 10,
}; };
// Parse road points to find midpoint and angle street_ways.entry(name.to_string())
let points = &way.points; .or_insert_with(Vec::new)
if points.len() < 16 { continue; } .push((way, priority));
let mut parsed_points: Vec<[f64; 2]> = Vec::new();
for chunk in 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;
parsed_points.push([lat, lon]);
}
if parsed_points.len() < 2 { continue; }
// Calculate midpoint
let mid_idx = parsed_points.len() / 2;
let mid_point = parsed_points[mid_idx];
// Calculate angle from road direction
let p1 = if mid_idx > 0 { parsed_points[mid_idx - 1] } else { parsed_points[0] };
let p2 = if mid_idx + 1 < parsed_points.len() { parsed_points[mid_idx + 1] } else { parsed_points[mid_idx] };
let (x1, y1) = project(p1[0], p1[1]);
let (x2, y2) = project(p2[0], p2[1]);
let dx = x2 - x1;
let dy = -(y2 - y1);
let mut angle_deg = (dy as f64).atan2(dx as f64).to_degrees();
// Keep text readable
if angle_deg > 90.0 { angle_deg -= 180.0; }
if angle_deg < -90.0 { angle_deg += 180.0; }
// Project midpoint to screen
let (mx, my) = project(mid_point[0], mid_point[1]);
let cx = mx * uniforms.params[0] + uniforms.params[2];
let cy = my * uniforms.params[1] + uniforms.params[3];
// Clip check
if cx < -1.5 || cx > 1.5 || cy < -1.5 || cy > 1.5 { continue; }
let css_x = (cx as f64 + 1.0) * 0.5 * client_width;
let css_y = (1.0 - cy as f64) * 0.5 * client_height;
candidates.push(LabelCandidate {
name: name.to_string(),
x: css_x,
y: css_y,
priority,
is_country: false,
rotation: angle_deg,
label_type: LabelType::Street,
category: "street".to_string(),
});
} }
} }
} }
} }
// For each unique street name, pick the longest segment as representative
for (street_name, ways_with_priority) in street_ways {
// Find the way with the most points (longest segment)
let best_way = ways_with_priority.iter()
.max_by_key(|(way, _)| way.points.len());
if let Some((way, priority)) = best_way {
let points = &way.points;
if points.len() < 16 { continue; }
let mut parsed_points: Vec<[f64; 2]> = Vec::new();
for chunk in 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;
parsed_points.push([lat, lon]);
}
if parsed_points.len() < 2 { continue; }
// Calculate midpoint for label placement
let mid_idx = parsed_points.len() / 2;
let mid_point = parsed_points[mid_idx];
// Get two points around midpoint for direction calculation
let p1_idx = mid_idx.saturating_sub(1);
let p2_idx = (mid_idx + 1).min(parsed_points.len() - 1);
let p1 = parsed_points[p1_idx];
let p2 = parsed_points[p2_idx];
// Project all three points to screen space (0-1 range)
let (mx_proj, my_proj) = project(mid_point[0], mid_point[1]);
let (x1_proj, y1_proj) = project(p1[0], p1[1]);
let (x2_proj, y2_proj) = project(p2[0], p2[1]);
// Apply camera transformation to get clip space (-1 to 1)
let cx = mx_proj * uniforms.params[0] + uniforms.params[2];
let cy = my_proj * uniforms.params[1] + uniforms.params[3];
let cx1 = x1_proj * uniforms.params[0] + uniforms.params[2];
let cy1 = y1_proj * uniforms.params[1] + uniforms.params[3];
let cx2 = x2_proj * uniforms.params[0] + uniforms.params[2];
let cy2 = y2_proj * uniforms.params[1] + uniforms.params[3];
// Clip check on midpoint
if cx < -1.5 || cx > 1.5 || cy < -1.5 || cy > 1.5 { continue; }
// Convert to CSS screen coordinates
// CSS: origin top-left, x right, y down
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_x1 = (cx1 as f64 + 1.0) * 0.5 * client_width;
let css_y1 = (1.0 - cy1 as f64) * 0.5 * client_height;
let css_x2 = (cx2 as f64 + 1.0) * 0.5 * client_width;
let css_y2 = (1.0 - cy2 as f64) * 0.5 * client_height;
// Calculate angle in CSS screen coordinates
// In CSS: x increases right, y increases down
// CSS rotate() is clockwise from positive x-axis
let dx = css_x2 - css_x1;
let dy = css_y2 - css_y1;
// atan2(dy, dx) gives angle from positive x-axis
// For dy > 0 (going down), we get positive angle, which rotates text clockwise
let mut angle_deg = dy.atan2(dx).to_degrees();
// Keep text readable (flip if upside down: angle between -90 and 90)
if angle_deg > 90.0 { angle_deg -= 180.0; }
if angle_deg < -90.0 { angle_deg += 180.0; }
// Determine highway type for CSS class (for font sizing)
let highway_type = way.tags.get("highway").map(|s| s.as_str()).unwrap_or("residential");
let road_class = match highway_type {
"motorway" | "trunk" => "motorway",
"primary" => "primary",
"secondary" => "secondary",
"tertiary" => "tertiary",
_ => "residential",
};
candidates.push(LabelCandidate {
name: street_name,
x: css_x,
y: css_y,
priority: *priority,
is_country: false,
rotation: angle_deg,
label_type: LabelType::Street,
category: road_class.to_string(),
});
}
}
// 4. Sort by Priority (High to Low) // 4. Sort by Priority (High to Low)
candidates.sort_by(|a, b| b.priority.cmp(&a.priority)); candidates.sort_by(|a, b| b.priority.cmp(&a.priority));
@@ -334,7 +379,7 @@ pub fn update_labels(
let class_name = match candidate.label_type { let class_name = match candidate.label_type {
LabelType::Country => "label label-country".to_string(), LabelType::Country => "label label-country".to_string(),
LabelType::City => "label label-city".to_string(), LabelType::City => "label label-city".to_string(),
LabelType::Street => "label label-street".to_string(), LabelType::Street => format!("label label-street label-street-{}", candidate.category),
LabelType::Poi => format!("label label-poi label-poi-{}", candidate.category), LabelType::Poi => format!("label label-poi label-poi-{}", candidate.category),
}; };

View File

@@ -34,18 +34,23 @@ use labels::update_labels;
use pipelines::{ use pipelines::{
Vertex, Vertex,
ColoredVertex, ColoredVertex,
create_simple_pipeline, RoadVertex,
create_building_pipeline, create_colored_building_pipeline,
create_water_pipeline, create_water_pipeline,
create_water_line_pipeline, create_water_line_pipeline,
create_road_motorway_outline_pipeline,
create_road_motorway_pipeline, create_road_motorway_pipeline,
create_road_primary_outline_pipeline,
create_road_primary_pipeline, create_road_primary_pipeline,
create_road_secondary_outline_pipeline,
create_road_secondary_pipeline, create_road_secondary_pipeline,
create_road_residential_outline_pipeline,
create_road_residential_pipeline, create_road_residential_pipeline,
create_landuse_green_pipeline, create_landuse_green_pipeline,
create_landuse_residential_pipeline, create_landuse_residential_pipeline,
create_sand_pipeline, create_sand_pipeline,
create_railway_pipeline, create_railway_pipeline,
generate_road_geometry,
}; };
#[wasm_bindgen(start)] #[wasm_bindgen(start)]
@@ -79,13 +84,40 @@ pub async fn run() {
None, None,
).await.unwrap(); ).await.unwrap();
let size = window.inner_size(); let win = web_sys::window().unwrap();
let dpr = win.device_pixel_ratio();
let inner_width = win.inner_width().unwrap().as_f64().unwrap();
let inner_height = win.inner_height().unwrap().as_f64().unwrap();
let width = (inner_width * dpr) as u32;
let height = (inner_height * dpr) as u32;
let max_dim = device.limits().max_texture_dimension_2d; let max_dim = device.limits().max_texture_dimension_2d;
let width = size.width.max(1).min(max_dim); let width = width.max(1).min(max_dim);
let height = size.height.max(1).min(max_dim); let height = height.max(1).min(max_dim);
// Explicitly resize the canvas/backing store to match physical pixels
if let Some(canvas) = window.canvas() {
canvas.set_width(width);
canvas.set_height(height);
}
let mut config = surface.get_default_config(&adapter, width, height).unwrap(); let mut config = surface.get_default_config(&adapter, width, height).unwrap();
surface.configure(&device, &config); surface.configure(&device, &config);
// Initial MSAA Texture
let mut msaa_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Multisampled Texture"),
size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 4,
dimension: wgpu::TextureDimension::D2,
format: config.format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let mut msaa_view = msaa_texture.create_view(&wgpu::TextureViewDescriptor::default());
// Initial Camera Setup // Initial Camera Setup
let camera = Arc::new(Mutex::new(Camera { let camera = Arc::new(Mutex::new(Camera {
x: 0.5307617, x: 0.5307617,
@@ -309,14 +341,23 @@ pub async fn run() {
} }
// Create Pipelines // Create Pipelines
let building_pipeline = create_building_pipeline(&device, &config.format, &camera_bind_group_layout); let building_pipeline = create_colored_building_pipeline(&device, &config.format, &camera_bind_group_layout);
let water_pipeline = create_water_pipeline(&device, &config.format, &camera_bind_group_layout); let water_pipeline = create_water_pipeline(&device, &config.format, &camera_bind_group_layout);
let water_line_pipeline = create_water_line_pipeline(&device, &config.format, &camera_bind_group_layout); let water_line_pipeline = create_water_line_pipeline(&device, &config.format, &camera_bind_group_layout);
let railway_pipeline = create_railway_pipeline(&device, &config.format, &camera_bind_group_layout); let railway_pipeline = create_railway_pipeline(&device, &config.format, &camera_bind_group_layout);
let motorway_pipeline = create_road_motorway_pipeline(&device, &config.format, &camera_bind_group_layout);
let primary_pipeline = create_road_primary_pipeline(&device, &config.format, &camera_bind_group_layout); // Road Pipelines (Outline & Fill)
let secondary_pipeline = create_road_secondary_pipeline(&device, &config.format, &camera_bind_group_layout); let motorway_outline = create_road_motorway_outline_pipeline(&device, &config.format, &camera_bind_group_layout);
let residential_pipeline = create_road_residential_pipeline(&device, &config.format, &camera_bind_group_layout); let motorway_fill = create_road_motorway_pipeline(&device, &config.format, &camera_bind_group_layout);
let primary_outline = create_road_primary_outline_pipeline(&device, &config.format, &camera_bind_group_layout);
let primary_fill = create_road_primary_pipeline(&device, &config.format, &camera_bind_group_layout);
let secondary_outline = create_road_secondary_outline_pipeline(&device, &config.format, &camera_bind_group_layout);
let secondary_fill = create_road_secondary_pipeline(&device, &config.format, &camera_bind_group_layout);
let residential_outline = create_road_residential_outline_pipeline(&device, &config.format, &camera_bind_group_layout);
let residential_fill = create_road_residential_pipeline(&device, &config.format, &camera_bind_group_layout);
let landuse_green_pipeline = create_landuse_green_pipeline(&device, &config.format, &camera_bind_group_layout); let landuse_green_pipeline = create_landuse_green_pipeline(&device, &config.format, &camera_bind_group_layout);
let landuse_residential_pipeline = create_landuse_residential_pipeline(&device, &config.format, &camera_bind_group_layout); let landuse_residential_pipeline = create_landuse_residential_pipeline(&device, &config.format, &camera_bind_group_layout);
let sand_pipeline = create_sand_pipeline(&device, &config.format, &camera_bind_group_layout); let sand_pipeline = create_sand_pipeline(&device, &config.format, &camera_bind_group_layout);
@@ -327,13 +368,41 @@ pub async fn run() {
match event { match event {
Event::WindowEvent { event, .. } => match event { Event::WindowEvent { event, .. } => match event {
WindowEvent::Resized(new_size) => { WindowEvent::Resized(_) => { // Ignore winit size, calculate manually
let win = web_sys::window().unwrap();
let dpr = win.device_pixel_ratio();
let inner_width = win.inner_width().unwrap().as_f64().unwrap();
let inner_height = win.inner_height().unwrap().as_f64().unwrap();
let width = (inner_width * dpr) as u32;
let height = (inner_height * dpr) as u32;
let max_dim = device.limits().max_texture_dimension_2d; let max_dim = device.limits().max_texture_dimension_2d;
let width = new_size.width.max(1).min(max_dim); let width = width.max(1).min(max_dim);
let height = new_size.height.max(1).min(max_dim); let height = height.max(1).min(max_dim);
// Explicitly resize the canvas/backing store to match physical pixels
if let Some(canvas) = window.canvas() {
canvas.set_width(width);
canvas.set_height(height);
}
config.width = width; config.width = width;
config.height = height; config.height = height;
surface.configure(&device, &config); surface.configure(&device, &config);
// Recreate MSAA texture
msaa_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Multisampled Texture"),
size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 4,
dimension: wgpu::TextureDimension::D2,
format: config.format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
msaa_view = msaa_texture.create_view(&wgpu::TextureViewDescriptor::default());
camera.lock().unwrap().aspect = width as f32 / height as f32; camera.lock().unwrap().aspect = width as f32 / height as f32;
window.request_redraw(); window.request_redraw();
} }
@@ -412,12 +481,12 @@ pub async fn run() {
for tile in tiles_to_process { for tile in tiles_to_process {
// Build vertex data for each feature type // Build vertex data for each feature type - roads use RoadVertex
let mut road_motorway_vertex_data: Vec<Vertex> = Vec::new(); let mut road_motorway_vertex_data: Vec<RoadVertex> = Vec::new();
let mut road_primary_vertex_data: Vec<Vertex> = Vec::new(); let mut road_primary_vertex_data: Vec<RoadVertex> = Vec::new();
let mut road_secondary_vertex_data: Vec<Vertex> = Vec::new(); let mut road_secondary_vertex_data: Vec<RoadVertex> = Vec::new();
let mut road_residential_vertex_data: Vec<Vertex> = Vec::new(); let mut road_residential_vertex_data: Vec<RoadVertex> = Vec::new();
let mut building_vertex_data: Vec<Vertex> = Vec::new(); let mut building_vertex_data: Vec<ColoredVertex> = Vec::new();
let mut landuse_green_vertex_data: Vec<Vertex> = Vec::new(); let mut landuse_green_vertex_data: Vec<Vertex> = Vec::new();
let mut landuse_residential_vertex_data: Vec<Vertex> = Vec::new(); let mut landuse_residential_vertex_data: Vec<Vertex> = Vec::new();
let mut landuse_sand_vertex_data: Vec<Vertex> = Vec::new(); let mut landuse_sand_vertex_data: Vec<Vertex> = Vec::new();
@@ -425,47 +494,99 @@ pub async fn run() {
let mut railway_vertex_data: Vec<ColoredVertex> = Vec::new(); let mut railway_vertex_data: Vec<ColoredVertex> = Vec::new();
let mut water_line_vertex_data: Vec<Vertex> = Vec::new(); let mut water_line_vertex_data: Vec<Vertex> = Vec::new();
// Process ways (roads) // Process ways (roads) - generate quad geometry for zoom-aware width
if let Some(ways) = state_guard.ways.get(&tile) { if let Some(ways) = state_guard.ways.get(&tile) {
for way in ways { for way in ways {
let highway = way.tags.get("highway").map(|s| s.as_str()); let highway = way.tags.get("highway").map(|s| s.as_str());
if highway.is_none() { continue; } if highway.is_none() { continue; }
let highway_type = highway.unwrap(); let highway_type = highway.unwrap();
let target = match highway_type {
// Road type: 0=motorway, 1=primary, 2=secondary, 3=residential
let road_type: f32 = match highway_type {
"motorway" | "motorway_link" | "trunk" | "trunk_link" => 0.0,
"primary" | "primary_link" => 1.0,
"secondary" | "secondary_link" => 2.0,
_ => 3.0,
};
let target: &mut Vec<RoadVertex> = match highway_type {
"motorway" | "motorway_link" | "trunk" | "trunk_link" => &mut road_motorway_vertex_data, "motorway" | "motorway_link" | "trunk" | "trunk_link" => &mut road_motorway_vertex_data,
"primary" | "primary_link" => &mut road_primary_vertex_data, "primary" | "primary_link" => &mut road_primary_vertex_data,
"secondary" | "secondary_link" => &mut road_secondary_vertex_data, "secondary" | "secondary_link" => &mut road_secondary_vertex_data,
_ => &mut road_residential_vertex_data, _ => &mut road_residential_vertex_data,
}; };
// Parse all points first // Parse lanes (default based on road type)
let mut road_points: Vec<Vertex> = Vec::new(); let default_lanes: f32 = match highway_type {
"motorway" | "trunk" => 4.0,
"motorway_link" | "trunk_link" | "primary" => 2.0,
_ => 2.0,
};
let lanes: f32 = way.tags.get("lanes")
.and_then(|s| s.parse().ok())
.unwrap_or(default_lanes);
// Parse all road centerline points
let mut centers: Vec<[f32; 2]> = Vec::new();
for chunk in way.points.chunks(8) { for chunk in way.points.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 lat = 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 lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap_or([0u8; 4]));
let (x, y) = project(lat as f64, lon as f64); let (x, y) = project(lat as f64, lon as f64);
road_points.push(Vertex { position: [x, y] }); centers.push([x, y]);
} }
// For LineList: push pairs of vertices for each segment (A-B, B-C, C-D, ...) // Generate connected geometry with miter joins
for i in 0..road_points.len().saturating_sub(1) { let geom = pipelines::roads::generate_road_geometry(&centers, lanes, road_type);
target.push(road_points[i]); target.extend(geom);
target.push(road_points[i + 1]);
}
} }
} }
// Process buildings // Process buildings with type-based colors
if let Some(buildings) = state_guard.buildings.get(&tile) { if let Some(buildings) = state_guard.buildings.get(&tile) {
for building in buildings { for building in buildings {
// Get building type from tags
let building_type = building.tags.get("building").map(|s| s.as_str()).unwrap_or("yes");
// Also check amenity tag for public buildings (hospitals, schools, etc.)
let amenity_type = building.tags.get("amenity").map(|s| s.as_str());
let is_public_amenity = matches!(amenity_type,
Some("hospital") | Some("school") | Some("university") | Some("college") |
Some("kindergarten") | Some("library") | Some("place_of_worship") |
Some("community_centre") | Some("townhall") | Some("police") |
Some("fire_station") | Some("courthouse") | Some("embassy")
);
// Assign color based on building type (light theme colors)
let color: [f32; 3] = if is_public_amenity {
// Public/Civic: light gray #e0ddd4
[0.88, 0.87, 0.83]
} else {
match building_type {
// Residential: cream #f2efe9
"house" | "apartments" | "residential" | "detached" | "semidetached_house" | "terrace" | "dormitory" =>
[0.95, 0.94, 0.91],
// Commercial: tan #e8e4dc
"commercial" | "retail" | "office" | "supermarket" | "kiosk" | "hotel" =>
[0.91, 0.89, 0.86],
// Industrial: gray-tan #d9d5cc
"industrial" | "warehouse" | "factory" | "manufacture" =>
[0.85, 0.84, 0.80],
// Public/Civic: light gray #e0ddd4
"school" | "hospital" | "university" | "government" | "public" | "church" | "cathedral" | "mosque" | "synagogue" | "temple" | "train_station" | "fire_station" | "police" | "library" | "kindergarten" =>
[0.88, 0.87, 0.83],
// Default: gray #d9d9d9
_ => [0.85, 0.85, 0.85],
}
};
for chunk in building.points.chunks(8) { for chunk in building.points.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 lat = 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 lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap_or([0u8; 4]));
let (x, y) = project(lat as f64, lon as f64); let (x, y) = project(lat as f64, lon as f64);
building_vertex_data.push(Vertex { position: [x, y] }); building_vertex_data.push(ColoredVertex { position: [x, y], color });
} }
} }
} }
@@ -732,16 +853,16 @@ pub async fn run() {
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None, label: None,
color_attachments: &[Some(wgpu::RenderPassColorAttachment { color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view, view: &msaa_view,
resolve_target: None, resolve_target: Some(&view),
ops: wgpu::Operations { ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color { load: wgpu::LoadOp::Clear(wgpu::Color {
r: if is_dark { 0.05 } else { 0.95 }, r: if is_dark { 0.05 } else { 0.957 },
g: if is_dark { 0.05 } else { 0.95 }, g: if is_dark { 0.05 } else { 0.957 },
b: if is_dark { 0.05 } else { 0.95 }, b: if is_dark { 0.05 } else { 0.957 },
a: 1.0, a: 1.0,
}), }),
store: wgpu::StoreOp::Store, store: wgpu::StoreOp::Discard,
}, },
})], })],
depth_stencil_attachment: None, depth_stencil_attachment: None,
@@ -799,9 +920,56 @@ pub async fn run() {
} }
} }
// 4. Draw Roads (Layered by importance) // 4. Draw Roads (Layered by importance with Outline/Fill pass for seamless joins)
// Residential
rpass.set_pipeline(&residential_pipeline); // --- PASS 1: OUTLINES (Bottom Layer) ---
// Order: Residential -> Secondary -> Primary -> Motorway
// Residential Outline
// rpass.set_pipeline(&residential_outline);
// rpass.set_bind_group(0, &camera_bind_group, &[]);
// for buffers in &tiles_to_render {
// if buffers.road_residential_vertex_count > 0 {
// rpass.set_vertex_buffer(0, buffers.road_residential_vertex_buffer.slice(..));
// rpass.draw(0..buffers.road_residential_vertex_count, 0..1);
// }
// }
// Secondary Outline
// rpass.set_pipeline(&secondary_outline);
// rpass.set_bind_group(0, &camera_bind_group, &[]);
// for buffers in &tiles_to_render {
// if buffers.road_secondary_vertex_count > 0 {
// rpass.set_vertex_buffer(0, buffers.road_secondary_vertex_buffer.slice(..));
// rpass.draw(0..buffers.road_secondary_vertex_count, 0..1);
// }
// }
// Primary Outline
// rpass.set_pipeline(&primary_outline);
// rpass.set_bind_group(0, &camera_bind_group, &[]);
// for buffers in &tiles_to_render {
// if buffers.road_primary_vertex_count > 0 {
// rpass.set_vertex_buffer(0, buffers.road_primary_vertex_buffer.slice(..));
// rpass.draw(0..buffers.road_primary_vertex_count, 0..1);
// }
// }
// Motorway Outline
// rpass.set_pipeline(&motorway_outline);
// rpass.set_bind_group(0, &camera_bind_group, &[]);
// for buffers in &tiles_to_render {
// if buffers.road_motorway_vertex_count > 0 {
// rpass.set_vertex_buffer(0, buffers.road_motorway_vertex_buffer.slice(..));
// rpass.draw(0..buffers.road_motorway_vertex_count, 0..1);
// }
// }
// --- PASS 2: FILLS (Top Layer) ---
// Order: Residential -> Secondary -> Primary -> Motorway
// Residential Fill
rpass.set_pipeline(&residential_fill);
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.road_residential_vertex_count > 0 { if buffers.road_residential_vertex_count > 0 {
@@ -809,8 +977,9 @@ pub async fn run() {
rpass.draw(0..buffers.road_residential_vertex_count, 0..1); rpass.draw(0..buffers.road_residential_vertex_count, 0..1);
} }
} }
// Secondary
rpass.set_pipeline(&secondary_pipeline); // Secondary Fill
rpass.set_pipeline(&secondary_fill);
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.road_secondary_vertex_count > 0 { if buffers.road_secondary_vertex_count > 0 {
@@ -818,8 +987,9 @@ pub async fn run() {
rpass.draw(0..buffers.road_secondary_vertex_count, 0..1); rpass.draw(0..buffers.road_secondary_vertex_count, 0..1);
} }
} }
// Primary
rpass.set_pipeline(&primary_pipeline); // Primary Fill
rpass.set_pipeline(&primary_fill);
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.road_primary_vertex_count > 0 { if buffers.road_primary_vertex_count > 0 {
@@ -827,8 +997,9 @@ pub async fn run() {
rpass.draw(0..buffers.road_primary_vertex_count, 0..1); rpass.draw(0..buffers.road_primary_vertex_count, 0..1);
} }
} }
// Motorway
rpass.set_pipeline(&motorway_pipeline); // Motorway Fill
rpass.set_pipeline(&motorway_fill);
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.road_motorway_vertex_count > 0 { if buffers.road_motorway_vertex_count > 0 {

View File

@@ -1,6 +1,6 @@
//! Building render pipeline //! Building render pipeline
use super::common::Vertex; use super::common::{Vertex, ColoredVertex};
pub fn create_building_pipeline( pub fn create_building_pipeline(
device: &wgpu::Device, device: &wgpu::Device,
@@ -35,10 +35,6 @@ pub fn create_building_pipeline(
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;
@@ -48,7 +44,7 @@ pub fn create_building_pipeline(
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
// Buildings: Light: #d9d9d9 (0.85), Dark: #333333 (0.2) // Buildings: Light: #d9d9d9 (0.85), Dark: #333333 (0.2)
let is_dark = camera.theme.x; let is_dark = camera.theme.x;
let color = mix(vec3<f32>(0.85, 0.85, 0.85), vec3<f32>(0.2, 0.2, 0.2), is_dark); let color = mix(vec3<f32>(0.92, 0.91, 0.90), vec3<f32>(0.2, 0.2, 0.2), is_dark);
return vec4<f32>(color, 1.0); return vec4<f32>(color, 1.0);
} }
"#)), "#)),
@@ -89,7 +85,102 @@ pub fn create_building_pipeline(
conservative: false, conservative: false,
}, },
depth_stencil: None, depth_stencil: None,
multisample: wgpu::MultisampleState::default(), multisample: wgpu::MultisampleState {
count: 4,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None,
})
}
/// Colored building pipeline - uses per-vertex color for different building types
pub fn create_colored_building_pipeline(
device: &wgpu::Device,
format: &wgpu::TextureFormat,
bind_group_layout: &wgpu::BindGroupLayout
) -> wgpu::RenderPipeline {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(r#"
struct CameraUniform {
params: vec4<f32>,
theme: vec4<f32>,
};
@group(0) @binding(0)
var<uniform> camera: CameraUniform;
struct VertexInput {
@location(0) position: vec2<f32>,
@location(1) color: vec3<f32>, // Per-vertex color (light theme)
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) v_color: vec3<f32>,
};
@vertex
fn vs_main(model: VertexInput) -> VertexOutput {
var out: VertexOutput;
let x = model.position.x * camera.params.x + camera.params.z;
let y = model.position.y * camera.params.y + camera.params.w;
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
out.v_color = model.color;
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let is_dark = camera.theme.x;
// Dark theme: darken the light theme color
let dark_color = in.v_color * 0.5;
let color = mix(in.v_color, dark_color, is_dark);
return vec4<f32>(color, 1.0);
}
"#)),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Colored Building Pipeline Layout"),
bind_group_layouts: &[bind_group_layout],
push_constant_ranges: &[],
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Colored Building Pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
buffers: &[ColoredVertex::desc()],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format: *format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
unclipped_depth: false,
polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 4,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None, multiview: None,
}) })
} }

View File

@@ -52,6 +52,48 @@ impl ColoredVertex {
} }
} }
/// GPU vertex for roads with center position, normal offset, and lanes
/// The shader will apply: final_position = center + normal * width_scale
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct RoadVertex {
pub center: [f32; 2], // Road centerline position
pub normal: [f32; 2], // Perpendicular offset direction (normalized, ±1 for each side)
pub lanes: f32, // Number of lanes (1-8)
pub road_type: f32, // 0=motorway, 1=primary, 2=secondary, 3=residential
}
impl RoadVertex {
pub fn desc() -> wgpu::VertexBufferLayout<'static> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<RoadVertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x2, // center
},
wgpu::VertexAttribute {
offset: std::mem::size_of::<[f32; 2]>() as wgpu::BufferAddress,
shader_location: 1,
format: wgpu::VertexFormat::Float32x2, // normal
},
wgpu::VertexAttribute {
offset: (std::mem::size_of::<[f32; 2]>() * 2) as wgpu::BufferAddress,
shader_location: 2,
format: wgpu::VertexFormat::Float32, // lanes
},
wgpu::VertexAttribute {
offset: (std::mem::size_of::<[f32; 2]>() * 2 + std::mem::size_of::<f32>()) as wgpu::BufferAddress,
shader_location: 3,
format: wgpu::VertexFormat::Float32, // road_type
}
]
}
}
}
/// Create a simple render pipeline with standard configuration /// Create a simple render pipeline with standard configuration
pub fn create_simple_pipeline( pub fn create_simple_pipeline(
device: &wgpu::Device, device: &wgpu::Device,
@@ -94,7 +136,62 @@ pub fn create_simple_pipeline(
conservative: false, conservative: false,
}, },
depth_stencil: None, depth_stencil: None,
multisample: wgpu::MultisampleState::default(), multisample: wgpu::MultisampleState {
count: 4,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None,
})
}
/// Create a road render pipeline that uses RoadVertex with lanes attribute
pub fn create_road_pipeline(
device: &wgpu::Device,
format: &wgpu::TextureFormat,
bind_group_layout: &wgpu::BindGroupLayout,
shader: &wgpu::ShaderModule,
label: &str,
topology: wgpu::PrimitiveTopology,
) -> wgpu::RenderPipeline {
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some(label),
bind_group_layouts: &[bind_group_layout],
push_constant_ranges: &[],
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: shader,
entry_point: "vs_main",
buffers: &[RoadVertex::desc()],
},
fragment: Some(wgpu::FragmentState {
module: shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format: *format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
unclipped_depth: false,
polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 4,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None, multiview: None,
}) })
} }

View File

@@ -35,7 +35,7 @@ pub fn create_landuse_green_pipeline(
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let is_dark = camera.theme.x; let is_dark = camera.theme.x;
// Green: Light #cdebb0, Dark #2d4a2d // Green: Light #cdebb0, Dark #2d4a2d
let color = mix(vec3<f32>(0.80, 0.92, 0.69), vec3<f32>(0.18, 0.29, 0.18), is_dark); let color = mix(vec3<f32>(0.804, 0.922, 0.690), vec3<f32>(0.18, 0.29, 0.18), is_dark);
return vec4<f32>(color, 1.0); return vec4<f32>(color, 1.0);
} }
"#)), "#)),

View File

@@ -7,15 +7,19 @@ pub mod roads;
pub mod landuse; pub mod landuse;
pub mod railway; pub mod railway;
pub use common::{Vertex, ColoredVertex, create_simple_pipeline}; pub use common::{Vertex, ColoredVertex, RoadVertex, create_simple_pipeline, create_road_pipeline};
pub use building::create_building_pipeline; pub use building::{create_building_pipeline, create_colored_building_pipeline};
pub use water::{create_water_pipeline, create_water_line_pipeline}; pub use water::{create_water_pipeline, create_water_line_pipeline};
pub use roads::{ pub use roads::{
create_road_motorway_pipeline, create_road_motorway_outline_pipeline,
create_road_primary_pipeline, create_road_motorway_pipeline,
create_road_secondary_pipeline, create_road_primary_outline_pipeline,
create_road_primary_pipeline,
create_road_secondary_outline_pipeline,
create_road_secondary_pipeline,
create_road_residential_outline_pipeline,
create_road_residential_pipeline, create_road_residential_pipeline,
create_road_mesh, generate_road_geometry,
}; };
pub use landuse::{create_landuse_green_pipeline, create_landuse_residential_pipeline, create_sand_pipeline}; pub use landuse::{create_landuse_green_pipeline, create_landuse_residential_pipeline, create_sand_pipeline};
pub use railway::create_railway_pipeline; pub use railway::create_railway_pipeline;

View File

@@ -95,7 +95,11 @@ pub fn create_railway_pipeline(
conservative: false, conservative: false,
}, },
depth_stencil: None, depth_stencil: None,
multisample: wgpu::MultisampleState::default(), multisample: wgpu::MultisampleState {
count: 4,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None, multiview: None,
}) })
} }

View File

@@ -1,52 +1,176 @@
//! Road render pipelines (motorway, primary, secondary, residential) //! Road render pipelines (motorway, primary, secondary, residential)
//! Now split into Outline and Fill passes for correct layering.
use super::common::{Vertex, create_simple_pipeline}; use super::common::{create_road_pipeline};
use crate::geo::project; // use crate::geo::project; // Unused
/// Create road mesh geometry (thick lines as triangles) // Helper for vector math
#[allow(dead_code)] fn normalize(v: [f32; 2]) -> [f32; 2] {
pub fn create_road_mesh(points: &[[f64; 2]], width: f32) -> Vec<Vertex> { 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]
}
/// Generate properly connected road geometry with miter joins
pub fn generate_road_geometry(points: &[[f32; 2]], lanes: f32, road_type: f32) -> Vec<super::common::RoadVertex> {
let mut vertices = Vec::new(); let mut vertices = Vec::new();
if points.len() < 2 { return vertices; } if points.len() < 2 { return vertices; }
for i in 0..points.len() - 1 { // Computes 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 p1 = points[i];
let p2 = points[i+1]; let p2 = points[i+1];
let dx = p2[0] - p1[0];
// Convert to projected coordinates (0..1) let dy = p2[1] - p1[1];
let (x1, y1) = project(p1[0], p1[1]); let len = (dx*dx + dy*dy).sqrt();
let (x2, y2) = project(p2[0], p2[1]); if len < 0.000001 {
segment_normals.push([0.0, 0.0]); // Degenerate
let dx = x2 - x1; } else {
let dy = y2 - y1; segment_normals.push([-dy/len, dx/len]);
let len = (dx * dx + dy * dy).sqrt(); }
// Skip invalid segments:
// 1. Very short segments that would create degenerate geometry
// 2. Segments where width > length (creates giant rectangles instead of roads)
if len < 0.00001 || len < (width * 2.0) as f32 { continue; }
// Normal vector scaled by width
let nx = -dy / len * width;
let ny = dx / len * width;
// 4 corners
let v1 = Vertex { position: [x1 + nx, y1 + ny] };
let v2 = Vertex { position: [x1 - nx, y1 - ny] };
let v3 = Vertex { position: [x2 + nx, y2 + ny] };
let v4 = Vertex { position: [x2 - nx, y2 - ny] };
// Triangle 1
vertices.push(v1);
vertices.push(v2);
vertices.push(v3);
// Triangle 2
vertices.push(v2);
vertices.push(v4);
vertices.push(v3);
} }
vertices
// Generate vertices (left/right pairs) for each point
// Store temporarily
let mut point_pairs = Vec::with_capacity(points.len() * 2);
for i in 0..points.len() {
// Find normal for this vertex (miter)
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 dot(n1, n1) == 0.0 { normal = n2; }
else if dot(n2, n2) == 0.0 { normal = n1; }
else {
let sum = [n1[0] + n2[0], n1[1] + n2[1]];
let miter = normalize(sum);
let d = dot(miter, n1);
if d.abs() < 0.1 { // Too sharp
normal = n1;
} else {
miter_len = 1.0 / d;
if miter_len > 4.0 { miter_len = 4.0; } // Relaxed clamp for sharper corners
normal = miter;
}
}
}
let p = points[i];
let nx = normal[0] * miter_len;
let ny = normal[1] * miter_len;
point_pairs.push(super::common::RoadVertex {
center: p,
normal: [nx, ny],
lanes,
road_type,
});
point_pairs.push(super::common::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 {
// Skip degenerate segment
if dot(segment_normals[i], segment_normals[i]) == 0.0 { continue; }
// Indices into point_pairs
let i_base = 2*i;
let j_base = 2*(i+1);
let v1 = point_pairs[i_base]; // Left curr
let v2 = point_pairs[i_base+1]; // Right curr
let v3 = point_pairs[j_base]; // Left next
let v4 = point_pairs[j_base+1]; // Right next
// Tri 1: v1, v2, v3
triangle_vertices.push(v1);
triangle_vertices.push(v2);
triangle_vertices.push(v3);
// Tri 2: v2, v4, v3
triangle_vertices.push(v2);
triangle_vertices.push(v4);
triangle_vertices.push(v3);
}
triangle_vertices
}
// ======================================================================================
// MOTORWAY
// ======================================================================================
pub fn create_road_motorway_outline_pipeline(
device: &wgpu::Device,
format: &wgpu::TextureFormat,
bind_group_layout: &wgpu::BindGroupLayout
) -> wgpu::RenderPipeline {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(r#"
struct CameraUniform {
params: vec4<f32>,
theme: vec4<f32>,
};
@group(0) @binding(0)
var<uniform> camera: CameraUniform;
struct VertexInput {
@location(0) center: vec2<f32>,
@location(1) normal: vec2<f32>,
@location(2) lanes: f32,
@location(3) road_type: f32,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) v_normal: vec2<f32>,
};
@vertex
fn vs_main(model: VertexInput) -> VertexOutput {
var out: VertexOutput;
// Motorways: Outline
let base_pixels = 4.0;
let lane_factor = model.lanes / 2.0;
let width = (base_pixels * lane_factor) / (camera.params.x * 4000.0);
let normal = model.normal;
let offset = normal * width;
let world_pos = model.center + offset;
let x = world_pos.x * camera.params.x + camera.params.z;
let y = world_pos.y * camera.params.y + camera.params.w;
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
// Highlight miter/anti-alias edge data if needed
out.v_normal = vec2<f32>(0.0, 0.0); // Placeholder
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let is_dark = camera.theme.x;
// Outline Color: Subtle Grey (Google Maps style)
let color = mix(vec3<f32>(0.8, 0.8, 0.8), vec3<f32>(0.3, 0.3, 0.3), is_dark);
return vec4<f32>(color, 1.0);
}
"#)),
});
create_road_pipeline(device, format, bind_group_layout, &shader, "Motorway Outline Pipeline", wgpu::PrimitiveTopology::TriangleList)
} }
pub fn create_road_motorway_pipeline( pub fn create_road_motorway_pipeline(
@@ -64,30 +188,95 @@ pub fn create_road_motorway_pipeline(
@group(0) @binding(0) @group(0) @binding(0)
var<uniform> camera: CameraUniform; var<uniform> camera: CameraUniform;
struct VertexInput { struct VertexInput {
@location(0) position: vec2<f32>, @location(0) center: vec2<f32>,
@location(1) normal: vec2<f32>,
@location(2) lanes: f32,
@location(3) road_type: f32,
}; };
struct VertexOutput { struct VertexOutput {
@builtin(position) clip_position: vec4<f32>, @builtin(position) clip_position: vec4<f32>,
@location(0) v_normal: vec2<f32>,
}; };
@vertex @vertex
fn vs_main(model: VertexInput) -> VertexOutput { fn vs_main(model: VertexInput) -> VertexOutput {
var out: VertexOutput; var out: VertexOutput;
let world_pos = model.position; // Motorways: Fill
let base_pixels = 3.0;
let lane_factor = model.lanes / 2.0;
let width = (base_pixels * lane_factor) / (camera.params.x * 4000.0);
let world_pos = model.center + model.normal * width;
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;
out.clip_position = vec4<f32>(x, y, 0.0, 1.0); out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
out.v_normal = model.normal;
return out; return out;
} }
@fragment @fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let is_dark = camera.theme.x; let is_dark = camera.theme.x;
// Light: #e990a0, Dark: #d97080 (slightly darker/richer) // Fill Color: Slight off-white/warm #fffcfa
let color = mix(vec3<f32>(0.91, 0.56, 0.63), vec3<f32>(0.85, 0.44, 0.50), is_dark); let color = mix(vec3<f32>(1.0, 0.99, 0.98), vec3<f32>(0.5, 0.5, 0.5), is_dark);
return vec4<f32>(color, 1.0); return vec4<f32>(color, 1.0);
} }
"#)), "#)),
}); });
create_simple_pipeline(device, format, bind_group_layout, &shader, "Motorway Pipeline", wgpu::PrimitiveTopology::LineList) create_road_pipeline(device, format, bind_group_layout, &shader, "Motorway Fill Pipeline", wgpu::PrimitiveTopology::TriangleList)
}
// ======================================================================================
// PRIMARY
// ======================================================================================
pub fn create_road_primary_outline_pipeline(
device: &wgpu::Device,
format: &wgpu::TextureFormat,
bind_group_layout: &wgpu::BindGroupLayout
) -> wgpu::RenderPipeline {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(r#"
struct CameraUniform {
params: vec4<f32>,
theme: vec4<f32>,
};
@group(0) @binding(0)
var<uniform> camera: CameraUniform;
struct VertexInput {
@location(0) center: vec2<f32>,
@location(1) normal: vec2<f32>,
@location(2) lanes: f32,
@location(3) road_type: f32,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) v_normal: vec2<f32>,
};
@vertex
fn vs_main(model: VertexInput) -> VertexOutput {
var out: VertexOutput;
// Primary: Outline
let base_pixels = 3.5;
let lane_factor = model.lanes / 2.0;
let width = (base_pixels * lane_factor) / (camera.params.x * 2000.0);
let world_pos = model.center + model.normal * width;
let x = world_pos.x * camera.params.x + camera.params.z;
let y = world_pos.y * camera.params.y + camera.params.w;
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
out.v_normal = model.normal;
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let is_dark = camera.theme.x;
// Outline: Subtle Grey
let color = mix(vec3<f32>(0.8, 0.8, 0.8), vec3<f32>(0.3, 0.3, 0.3), is_dark);
return vec4<f32>(color, 1.0);
}
"#)),
});
create_road_pipeline(device, format, bind_group_layout, &shader, "Primary Road Outline Pipeline", wgpu::PrimitiveTopology::TriangleList)
} }
pub fn create_road_primary_pipeline( pub fn create_road_primary_pipeline(
@@ -105,30 +294,97 @@ pub fn create_road_primary_pipeline(
@group(0) @binding(0) @group(0) @binding(0)
var<uniform> camera: CameraUniform; var<uniform> camera: CameraUniform;
struct VertexInput { struct VertexInput {
@location(0) position: vec2<f32>, @location(0) center: vec2<f32>,
@location(1) normal: vec2<f32>,
@location(2) lanes: f32,
@location(3) road_type: f32,
}; };
struct VertexOutput { struct VertexOutput {
@builtin(position) clip_position: vec4<f32>, @builtin(position) clip_position: vec4<f32>,
@location(0) v_normal: vec2<f32>,
}; };
@vertex @vertex
fn vs_main(model: VertexInput) -> VertexOutput { fn vs_main(model: VertexInput) -> VertexOutput {
var out: VertexOutput; var out: VertexOutput;
let world_pos = model.position; // Primary: Fill
let base_pixels = 2.5;
let lane_factor = model.lanes / 2.0;
let width = (base_pixels * lane_factor) / (camera.params.x * 2000.0);
let world_pos = model.center + model.normal * width;
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;
out.clip_position = vec4<f32>(x, y, 0.0, 1.0); out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
out.v_normal = model.normal;
return out; return out;
} }
@fragment @fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let is_dark = camera.theme.x; let is_dark = camera.theme.x;
// Light: #fdbf6f, Dark: #e09f3f // Fill
// Fill
let color = mix(vec3<f32>(0.99, 0.75, 0.44), vec3<f32>(0.88, 0.62, 0.25), is_dark); let color = mix(vec3<f32>(0.99, 0.75, 0.44), vec3<f32>(0.88, 0.62, 0.25), is_dark);
return vec4<f32>(color, 1.0); return vec4<f32>(color, 1.0);
} }
"#)), "#)),
}); });
create_simple_pipeline(device, format, bind_group_layout, &shader, "Primary Road Pipeline", wgpu::PrimitiveTopology::LineList) create_road_pipeline(device, format, bind_group_layout, &shader, "Primary Road Fill Pipeline", wgpu::PrimitiveTopology::TriangleList)
}
// ======================================================================================
// SECONDARY
// ======================================================================================
pub fn create_road_secondary_outline_pipeline(
device: &wgpu::Device,
format: &wgpu::TextureFormat,
bind_group_layout: &wgpu::BindGroupLayout
) -> wgpu::RenderPipeline {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(r#"
struct CameraUniform {
params: vec4<f32>,
theme: vec4<f32>,
};
@group(0) @binding(0)
var<uniform> camera: CameraUniform;
struct VertexInput {
@location(0) center: vec2<f32>,
@location(1) normal: vec2<f32>,
@location(2) lanes: f32,
@location(3) road_type: f32,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) v_normal: vec2<f32>,
};
@vertex
fn vs_main(model: VertexInput) -> VertexOutput {
var out: VertexOutput;
// Secondary: Outline
let base_pixels = 4.0;
let lane_factor = model.lanes / 2.0;
let width = (base_pixels * lane_factor) / (camera.params.x * 500.0);
let world_pos = model.center + model.normal * width;
let x = world_pos.x * camera.params.x + camera.params.z;
let y = world_pos.y * camera.params.y + camera.params.w;
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
out.v_normal = model.normal;
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let is_dark = camera.theme.x;
// Outline: Gray
// Outline: Gray
let color = mix(vec3<f32>(0.5, 0.5, 0.5), vec3<f32>(0.2, 0.2, 0.2), is_dark);
return vec4<f32>(color, 1.0);
}
"#)),
});
create_road_pipeline(device, format, bind_group_layout, &shader, "Secondary Road Outline Pipeline", wgpu::PrimitiveTopology::TriangleList)
} }
pub fn create_road_secondary_pipeline( pub fn create_road_secondary_pipeline(
@@ -146,30 +402,97 @@ pub fn create_road_secondary_pipeline(
@group(0) @binding(0) @group(0) @binding(0)
var<uniform> camera: CameraUniform; var<uniform> camera: CameraUniform;
struct VertexInput { struct VertexInput {
@location(0) position: vec2<f32>, @location(0) center: vec2<f32>,
@location(1) normal: vec2<f32>,
@location(2) lanes: f32,
@location(3) road_type: f32,
}; };
struct VertexOutput { struct VertexOutput {
@builtin(position) clip_position: vec4<f32>, @builtin(position) clip_position: vec4<f32>,
@location(0) v_normal: vec2<f32>,
}; };
@vertex @vertex
fn vs_main(model: VertexInput) -> VertexOutput { fn vs_main(model: VertexInput) -> VertexOutput {
var out: VertexOutput; var out: VertexOutput;
let world_pos = model.position; // Secondary: Fill
let base_pixels = 3.0;
let lane_factor = model.lanes / 2.0;
let width = (base_pixels * lane_factor) / (camera.params.x * 500.0);
let world_pos = model.center + model.normal * width;
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;
out.clip_position = vec4<f32>(x, y, 0.0, 1.0); out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
out.v_normal = model.normal;
return out; return out;
} }
@fragment @fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let is_dark = camera.theme.x; let is_dark = camera.theme.x;
// Light: White, Dark: #444444 // Fill: White/Dark Gray
let color = mix(vec3<f32>(1.0, 1.0, 1.0), vec3<f32>(0.27, 0.27, 0.27), is_dark); // Fill: White/Dark Gray
let color = mix(vec3<f32>(1.0, 1.0, 1.0), vec3<f32>(0.35, 0.35, 0.35), is_dark);
return vec4<f32>(color, 1.0); return vec4<f32>(color, 1.0);
} }
"#)), "#)),
}); });
create_simple_pipeline(device, format, bind_group_layout, &shader, "Secondary Road Pipeline", wgpu::PrimitiveTopology::LineList) create_road_pipeline(device, format, bind_group_layout, &shader, "Secondary Road Fill Pipeline", wgpu::PrimitiveTopology::TriangleList)
}
// ======================================================================================
// RESIDENTIAL
// ======================================================================================
pub fn create_road_residential_outline_pipeline(
device: &wgpu::Device,
format: &wgpu::TextureFormat,
bind_group_layout: &wgpu::BindGroupLayout
) -> wgpu::RenderPipeline {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(r#"
struct CameraUniform {
params: vec4<f32>,
theme: vec4<f32>,
};
@group(0) @binding(0)
var<uniform> camera: CameraUniform;
struct VertexInput {
@location(0) center: vec2<f32>,
@location(1) normal: vec2<f32>,
@location(2) lanes: f32,
@location(3) road_type: f32,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) v_normal: vec2<f32>,
};
@vertex
fn vs_main(model: VertexInput) -> VertexOutput {
var out: VertexOutput;
// Residential: Outline
let base_pixels = 3.2;
let lane_factor = model.lanes / 2.0;
let width = (base_pixels * lane_factor) / (camera.params.x * 250.0);
let world_pos = model.center + model.normal * width;
let x = world_pos.x * camera.params.x + camera.params.z;
let y = world_pos.y * camera.params.y + camera.params.w;
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
out.v_normal = model.normal;
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let is_dark = camera.theme.x;
// Outline: Gray
// Outline: Gray
let color = mix(vec3<f32>(0.5, 0.5, 0.5), vec3<f32>(0.2, 0.2, 0.2), is_dark);
return vec4<f32>(color, 1.0);
}
"#)),
});
create_road_pipeline(device, format, bind_group_layout, &shader, "Residential Road Outline Pipeline", wgpu::PrimitiveTopology::TriangleList)
} }
pub fn create_road_residential_pipeline( pub fn create_road_residential_pipeline(
@@ -187,28 +510,39 @@ pub fn create_road_residential_pipeline(
@group(0) @binding(0) @group(0) @binding(0)
var<uniform> camera: CameraUniform; var<uniform> camera: CameraUniform;
struct VertexInput { struct VertexInput {
@location(0) position: vec2<f32>, @location(0) center: vec2<f32>,
@location(1) normal: vec2<f32>,
@location(2) lanes: f32,
@location(3) road_type: f32,
}; };
struct VertexOutput { struct VertexOutput {
@builtin(position) clip_position: vec4<f32>, @builtin(position) clip_position: vec4<f32>,
@location(0) v_normal: vec2<f32>,
}; };
@vertex @vertex
fn vs_main(model: VertexInput) -> VertexOutput { fn vs_main(model: VertexInput) -> VertexOutput {
var out: VertexOutput; var out: VertexOutput;
let world_pos = model.position; // Residential: Fill
let base_pixels = 2.2;
let lane_factor = model.lanes / 2.0;
let width = (base_pixels * lane_factor) / (camera.params.x * 250.0);
let world_pos = model.center + model.normal * width;
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;
out.clip_position = vec4<f32>(x, y, 0.0, 1.0); out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
out.v_normal = model.normal;
return out; return out;
} }
@fragment @fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let is_dark = camera.theme.x; let is_dark = camera.theme.x;
// Light: White, Dark: #333333 // Fill: White/Dark Gray
let color = mix(vec3<f32>(1.0, 1.0, 1.0), vec3<f32>(0.2, 0.2, 0.2), is_dark); // Fill: White/Dark Gray
let color = mix(vec3<f32>(1.0, 1.0, 1.0), vec3<f32>(0.35, 0.35, 0.35), is_dark);
return vec4<f32>(color, 1.0); return vec4<f32>(color, 1.0);
} }
"#)), "#)),
}); });
create_simple_pipeline(device, format, bind_group_layout, &shader, "Residential Road Pipeline", wgpu::PrimitiveTopology::LineList) create_road_pipeline(device, format, bind_group_layout, &shader, "Residential Road Fill Pipeline", wgpu::PrimitiveTopology::TriangleList)
} }

View File

@@ -48,7 +48,7 @@ pub fn create_water_pipeline(
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
// Water: Light: #9ecaff, Dark: #1a2639 // Water: Light: #9ecaff, Dark: #1a2639
let is_dark = camera.theme.x; let is_dark = camera.theme.x;
let color = mix(vec3<f32>(0.62, 0.79, 1.0), vec3<f32>(0.1, 0.15, 0.22), is_dark); let color = mix(vec3<f32>(0.647, 0.749, 0.867), vec3<f32>(0.1, 0.15, 0.22), is_dark);
return vec4<f32>(color, 1.0); return vec4<f32>(color, 1.0);
} }
"#)), "#)),
@@ -99,7 +99,11 @@ pub fn create_water_pipeline(
conservative: false, conservative: false,
}, },
depth_stencil: None, depth_stencil: None,
multisample: wgpu::MultisampleState::default(), multisample: wgpu::MultisampleState {
count: 4,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None, multiview: None,
}) })
} }
@@ -137,7 +141,7 @@ pub fn create_water_line_pipeline(
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let is_dark = camera.theme.x; let is_dark = camera.theme.x;
// Light: #a5bfdd (same/similar to water), Dark: #4a6fa5 // Light: #a5bfdd (same/similar to water), Dark: #4a6fa5
let color = mix(vec3<f32>(0.66, 0.82, 0.96), vec3<f32>(0.29, 0.44, 0.65), is_dark); let color = mix(vec3<f32>(0.647, 0.749, 0.867), vec3<f32>(0.29, 0.44, 0.65), is_dark);
return vec4<f32>(color, 1.0); return vec4<f32>(color, 1.0);
} }
"#)), "#)),

View File

@@ -632,10 +632,10 @@ async fn main() -> Result<()> {
// Apply simplification based on zoom level // Apply simplification based on zoom level
let base_epsilon = match zoom { let base_epsilon = match zoom {
2 => 0.0001, 2 => 0.01, // Was 0.0001 (~11m) -> Now ~1km
4 => 0.00005, 4 => 0.002, // Was 0.00005 (~5m) -> Now ~200m
6 => 0.00002, 6 => 0.0005, // Was 0.00002 (~2m) -> Now ~50m
9 => 0.00001, 9 => 0.0001, // Was 0.00001 (~1m) -> Now ~10m
12 => 0.000005, 12 => 0.000005,
_ => 0.0, _ => 0.0,
}; };